diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e210db33d..4333ef0e4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,11 +11,7 @@ updates: - "A-Dependencies" - "Z-Deps-Backend" schedule: - interval: "daily" - ignore: - # We plan to remove apalis soon, let's ignore it for now - - dependency-name: "apalis" - - dependency-name: "apalis-*" + interval: "monthly" groups: axum: patterns: @@ -53,7 +49,7 @@ updates: - "A-Dependencies" - "Z-Deps-CI" schedule: - interval: "daily" + interval: "monthly" cooldown: default-days: 14 @@ -63,7 +59,7 @@ updates: - "A-Dependencies" - "Z-Deps-Frontend" schedule: - interval: "daily" + interval: "monthly" groups: storybook: patterns: diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6b25b3f6b..2991a558e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,9 +27,12 @@ env: CARGO_NET_GIT_FETCH_WITH_CLI: "true" SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" - IMAGE: ghcr.io/element-hq/matrix-authentication-service BUILDCACHE: ghcr.io/element-hq/matrix-authentication-service/buildcache - DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + # metadata-action defaults to `manifest`, which `docker buildx imagetools + # create --annotation` refuses with "manifest annotations are not supported + # yet". We only want annotations on the manifest list anyway, so narrow it + # to `index`. + DOCKER_METADATA_ANNOTATIONS_LEVELS: index jobs: compute-version: @@ -208,32 +211,30 @@ jobs: name: mas-cli-x86_64-linux path: mas-cli-x86_64-linux.tar.gz - build-image: - name: Build and push Docker image + compute-image-meta: + name: Compute Docker image metadata if: github.event_name == 'push' || github.event.label.name == 'Z-Build-Workflow' runs-on: ubuntu-24.04 - outputs: - metadata: ${{ steps.output.outputs.metadata }} - permissions: contents: read - packages: write - id-token: write - needs: - - compute-version - - env: - VERGEN_GIT_DESCRIBE: ${{ needs.compute-version.outputs.describe }} - SOURCE_DATE_EPOCH: ${{ needs.compute-version.outputs.timestamp }} + outputs: + regular-tags: ${{ steps.meta.outputs.tags }} + regular-annotations: ${{ steps.meta.outputs.annotations }} + debug-tags: ${{ steps.meta-debug.outputs.tags }} + debug-annotations: ${{ steps.meta-debug.outputs.annotations }} steps: - name: Docker meta id: meta uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: - images: "${{ env.IMAGE }}" + # The oci-push registry login requires Tailscale + Vault, which only + # works for `push` events (PR-labelled runs lack the right OIDC token). + images: | + ghcr.io/element-hq/matrix-authentication-service + ${{ github.event_name == 'push' && 'oci-push.vpn.infra.element.io/matrix-authentication-service' || '' }} bake-target: docker-metadata-action flavor: | latest=auto @@ -244,12 +245,23 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha + # GitHub's license detection (via Licensee) can only report a single + # SPDX identifier and still emits the legacy `AGPL-3.0` form, which + # `metadata-action` would otherwise propagate as-is. Override it with + # the project's actual dual-license SPDX expression so the image + # advertises both halves of the dual licensing. + labels: | + org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial + annotations: | + org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial - name: Docker meta (debug variant) id: meta-debug uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: - images: "${{ env.IMAGE }}" + images: | + ghcr.io/element-hq/matrix-authentication-service + ${{ github.event_name == 'push' && 'oci-push.vpn.infra.element.io/matrix-authentication-service' || '' }} bake-target: docker-metadata-action-debug flavor: | latest=auto @@ -261,10 +273,63 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha + labels: | + org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial + annotations: | + org.opencontainers.image.licenses=AGPL-3.0-only OR LicenseRef-Element-Commercial - - name: Setup Cosign - uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + # Stage the labels bake files under predictable names (the metadata-action + # writes them under a random temp dir with the same base name) and ship + # them to `build-image` as an artifact. We deliberately only pass through + # `bake-file-labels` (not `bake-file-annotations`): the per-arch images + # then carry the same config labels as today, while annotations are only + # applied at the index level in `finalize-image` — matching `:latest`'s + # shape and sidestepping `metadata-action`'s empty-value annotations + # which would otherwise trip `docker buildx imagetools create`. + - name: Stage bake files + env: + REGULAR_FILE: ${{ steps.meta.outputs.bake-file-labels }} + DEBUG_FILE: ${{ steps.meta-debug.outputs.bake-file-labels }} + run: | + mkdir -p /tmp/bake + cp "$REGULAR_FILE" /tmp/bake/regular.json + cp "$DEBUG_FILE" /tmp/bake/debug.json + - name: Upload bake files + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: bake-files + path: /tmp/bake/ + retention-days: 1 + + build-image: + name: Build Docker image (${{ matrix.arch }}) + if: github.event_name == 'push' || github.event.label.name == 'Z-Build-Workflow' + runs-on: ubuntu-24.04 + + permissions: + contents: read + packages: write + id-token: write + + needs: + - compute-version + - compute-image-meta + + strategy: + fail-fast: false + matrix: + arch: [amd64, arm64] + + env: + VERGEN_GIT_DESCRIBE: ${{ needs.compute-version.outputs.describe }} + SOURCE_DATE_EPOCH: ${{ needs.compute-version.outputs.timestamp }} + # Comma-separated list of registries to push each per-arch image to. + # The oci-push registry is only included on `push` events because the + # Tailscale + Vault login below requires the right OIDC token. + IMAGES: ghcr.io/element-hq/matrix-authentication-service${{ github.event_name == 'push' && ',oci-push.vpn.infra.element.io/matrix-authentication-service' || '' }} + + steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: @@ -279,29 +344,218 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push + # The Element OCI Registry is only reachable via Tailscale, and the Vault + # JWT exchange relies on a GitHub OIDC token issued from a `push` event. + # PR-labelled builds (`Z-Build-Workflow`) skip this and push only to ghcr. + - name: Tailscale + if: github.event_name == 'push' + uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4.1.1 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + audience: ${{ secrets.TS_AUDIENCE }} + tags: tag:github-actions + + - name: Compute vault jwt role name + id: vault-jwt-role + if: github.event_name == 'push' + run: | + echo "role_name=github_service_management_$( echo "${{ github.repository }}" | sed -r 's|[/-]|_|g')" | tee -a "$GITHUB_OUTPUT" + + - name: Get team registry token + id: import-secrets + if: github.event_name == 'push' + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0 + with: + url: https://vault.infra.ci.i.element.dev + role: ${{ steps.vault-jwt-role.outputs.role_name }} + path: service-management/github-actions + jwtGithubAudience: https://vault.infra.ci.i.element.dev + method: jwt + secrets: | + services/backend-repositories/secret/data/oci.element.io username | OCI_USERNAME ; + services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; + + - name: Login to Element OCI Registry + if: github.event_name == 'push' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: oci-push.vpn.infra.element.io + username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} + password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} + + - name: Download bake files + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: bake-files + path: /tmp/bake + + - name: Build and push by digest id: bake uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0 + env: + # By default, docker bake will add provenance information to the + # metadata output. This makes the output larger and may exceed the + # shell ARG_MAX limit. Disabling through this environment variable + # disables provenance in the metadata while still attaching provenance + # attestations to the image we push. + # https://github.com/docker/bake-action/issues/239#issuecomment-3828170326 + BUILDX_METADATA_PROVENANCE: disabled with: files: | ./docker-bake.hcl - cwd://${{ steps.meta.outputs.bake-file }} - cwd://${{ steps.meta-debug.outputs.bake-file }} + cwd:///tmp/bake/regular.json + cwd:///tmp/bake/debug.json set: | - base.output=type=image,push=true - base.cache-from=type=registry,ref=${{ env.BUILDCACHE }}:buildcache - base.cache-to=type=registry,ref=${{ env.BUILDCACHE }}:buildcache,mode=max + *.platform=linux/${{ matrix.arch }} + *.output=type=image,"name=${{ env.IMAGES }}",push-by-digest=true,name-canonical=true,push=true + *.cache-from=type=registry,ref=${{ env.BUILDCACHE }}:buildcache-${{ matrix.arch }} + *.cache-to=type=registry,ref=${{ env.BUILDCACHE }}:buildcache-${{ matrix.arch }},mode=max - - name: Transform bake output - # This transforms the ouput to an object which looks like this: - # { reguar: { digest: "…", tags: ["…", "…"] }, debug: { digest: "…", tags: ["…"] }, … } - id: output - run: | - echo 'metadata<> $GITHUB_OUTPUT - echo "$STEPS_BAKE_OUTPUTS_METADATA" | jq -c 'with_entries(select(.value | (type == "object" and has("containerimage.digest")))) | map_values({ digest: .["containerimage.digest"], tags: (.["image.name"] | split(",")) })' >> $GITHUB_OUTPUT - echo 'EOF' >> $GITHUB_OUTPUT + # We use github-script rather than shelling out to jq because the bake + # metadata can exceed the shell ARG_MAX limit when inherited as an env + # var by an exec'd jq. + - name: Export digests + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: STEPS_BAKE_OUTPUTS_METADATA: ${{ steps.bake.outputs.metadata }} + ARCH: ${{ matrix.arch }} + with: + script: | + const fs = require('node:fs'); + const path = require('node:path'); + const bakeOutput = JSON.parse(process.env.STEPS_BAKE_OUTPUTS_METADATA); + const arch = process.env.ARCH; + fs.mkdirSync('/tmp/digests', { recursive: true }); + for (const target of ['regular', 'debug']) { + const digest = bakeOutput[target]?.['containerimage.digest']; + if (!digest) { + throw new Error(`Missing containerimage.digest for target ${target}`); + } + fs.writeFileSync(path.join('/tmp/digests', `${target}-${arch}`), digest); + } + + - name: Upload digests + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: digests-${{ matrix.arch }} + path: /tmp/digests/* + retention-days: 1 + + finalize-image: + name: Create multi-arch manifests + if: github.event_name == 'push' || github.event.label.name == 'Z-Build-Workflow' + runs-on: ubuntu-24.04 + + needs: + - build-image + - compute-image-meta + + outputs: + metadata: ${{ steps.output.outputs.metadata }} + + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Download digests + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: digests-* + path: /tmp/digests + # Collect digests from both amd64 and arm64 builds + merge-multiple: true + + - name: Setup Cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # See `build-image` for why the Element OCI Registry login is gated on + # `push` events. + - name: Tailscale + if: github.event_name == 'push' + uses: tailscale/github-action@53acf823325fe9ca47f4cdaa951f90b4b0de5bb9 # v4.1.1 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + audience: ${{ secrets.TS_AUDIENCE }} + tags: tag:github-actions + + - name: Compute vault jwt role name + id: vault-jwt-role + if: github.event_name == 'push' + run: | + echo "role_name=github_service_management_$( echo "${{ github.repository }}" | sed -r 's|[/-]|_|g')" | tee -a "$GITHUB_OUTPUT" + + - name: Get team registry token + id: import-secrets + if: github.event_name == 'push' + uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b # v3.4.0 + with: + url: https://vault.infra.ci.i.element.dev + role: ${{ steps.vault-jwt-role.outputs.role_name }} + path: service-management/github-actions + jwtGithubAudience: https://vault.infra.ci.i.element.dev + method: jwt + secrets: | + services/backend-repositories/secret/data/oci.element.io username | OCI_USERNAME ; + services/backend-repositories/secret/data/oci.element.io password | OCI_PASSWORD ; + + - name: Login to Element OCI Registry + if: github.event_name == 'push' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: oci-push.vpn.infra.element.io + username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} + password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} + + - name: Create regular manifest + id: regular + env: + TAGS: ${{ needs.compute-image-meta.outputs.regular-tags }} + ANNOTATIONS: ${{ needs.compute-image-meta.outputs.regular-annotations }} + run: | + # Construct the `imagetools create` command line from the tag and annotation inputs. + args=() + + # Add a `-t ` argument for each non-empty tag. + while IFS= read -r t; do [[ -n $t ]] && args+=(-t "$t"); done <<< "$TAGS" + + # Add a `--annotation =` argument for each non-empty annotation + while IFS= read -r a; do [[ -n $a && $a != *= ]] && args+=(--annotation "$a"); done <<< "$ANNOTATIONS" + + docker buildx imagetools create "${args[@]}" \ + "ghcr.io/element-hq/matrix-authentication-service@$(cat /tmp/digests/regular-amd64)" \ + "ghcr.io/element-hq/matrix-authentication-service@$(cat /tmp/digests/regular-arm64)" \ + --metadata-file regular-metadata.json + + # `imagetools create` wrote the digest to regular-metadata.json + echo "digest=$(jq -r '.["containerimage.descriptor"].digest' regular-metadata.json)" >> "$GITHUB_OUTPUT" + + - name: Create debug manifest + id: debug + env: + TAGS: ${{ needs.compute-image-meta.outputs.debug-tags }} + ANNOTATIONS: ${{ needs.compute-image-meta.outputs.debug-annotations }} + run: | + # See comments in regular manifest creation for argument construction. + args=() + while IFS= read -r t; do [[ -n $t ]] && args+=(-t "$t"); done <<< "$TAGS" + while IFS= read -r a; do [[ -n $a && $a != *= ]] && args+=(--annotation "$a"); done <<< "$ANNOTATIONS" + docker buildx imagetools create "${args[@]}" \ + "ghcr.io/element-hq/matrix-authentication-service@$(cat /tmp/digests/debug-amd64)" \ + "ghcr.io/element-hq/matrix-authentication-service@$(cat /tmp/digests/debug-arm64)" \ + --metadata-file debug-metadata.json + echo "digest=$(jq -r '.["containerimage.descriptor"].digest' debug-metadata.json)" >> "$GITHUB_OUTPUT" - name: Sign the images with GitHub Actions provided token # Only sign on tags and on commits on main branch @@ -310,13 +564,38 @@ jobs: && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') env: - REGULAR_DIGEST: ${{ steps.output.outputs.metadata && fromJSON(steps.output.outputs.metadata).regular.digest }} - DEBUG_DIGEST: ${{ steps.output.outputs.metadata && fromJSON(steps.output.outputs.metadata).debug.digest }} + REGULAR_DIGEST: ${{ steps.regular.outputs.digest }} + DEBUG_DIGEST: ${{ steps.debug.outputs.digest }} run: |- cosign sign --yes \ - "$IMAGE@$REGULAR_DIGEST" \ - "$IMAGE@$DEBUG_DIGEST" \ + "ghcr.io/element-hq/matrix-authentication-service@$REGULAR_DIGEST" \ + "ghcr.io/element-hq/matrix-authentication-service@$DEBUG_DIGEST" + cosign sign --yes \ + "oci-push.vpn.infra.element.io/matrix-authentication-service@$REGULAR_DIGEST" \ + "oci-push.vpn.infra.element.io/matrix-authentication-service@$DEBUG_DIGEST" + + - name: Output metadata + id: output + env: + REGULAR_DIGEST: ${{ steps.regular.outputs.digest }} + DEBUG_DIGEST: ${{ steps.debug.outputs.digest }} + REGULAR_TAGS: ${{ needs.compute-image-meta.outputs.regular-tags }} + DEBUG_TAGS: ${{ needs.compute-image-meta.outputs.debug-tags }} + run: | + # Convert the newline-separated tag lists into JSON arrays. + regular_tags=$(jq -Rnc '[inputs | select(length > 0)]' <<< "$REGULAR_TAGS") + debug_tags=$(jq -Rnc '[inputs | select(length > 0)]' <<< "$DEBUG_TAGS") + { + echo 'metadata<> "$GITHUB_OUTPUT" release: name: Release @@ -324,7 +603,8 @@ jobs: runs-on: ubuntu-24.04 needs: - assemble-archives - - build-image + - finalize-image + steps: - name: Download the artifacts from the previous job uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -344,11 +624,12 @@ jobs: - Digest: ``` - ${{ env.IMAGE }}@${{ fromJSON(needs.build-image.outputs.metadata).regular.digest }} + ghcr.io/element-hq/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).regular.digest }} + oci.element.io/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).regular.digest }} ``` - Tags: ``` - ${{ join(fromJSON(needs.build-image.outputs.metadata).regular.tags, ' + ${{ join(fromJSON(needs.finalize-image.outputs.metadata).regular.tags, ' ') }} ``` @@ -356,11 +637,12 @@ jobs: - Digest: ``` - ${{ env.IMAGE }}@${{ fromJSON(needs.build-image.outputs.metadata).debug.digest }} + ghcr.io/element-hq/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).debug.digest }} + oci.element.io/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).debug.digest }} ``` - Tags: ``` - ${{ join(fromJSON(needs.build-image.outputs.metadata).debug.tags, ' + ${{ join(fromJSON(needs.finalize-image.outputs.metadata).debug.tags, ' ') }} ``` @@ -376,7 +658,7 @@ jobs: needs: - assemble-archives - - build-image + - finalize-image permissions: contents: write @@ -422,11 +704,12 @@ jobs: - Digest: ``` - ${{ env.IMAGE }}@${{ fromJSON(needs.build-image.outputs.metadata).regular.digest }} + ghcr.io/element-hq/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).regular.digest }} + oci.element.io/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).regular.digest }} ``` - Tags: ``` - ${{ join(fromJSON(needs.build-image.outputs.metadata).regular.tags, ' + ${{ join(fromJSON(needs.finalize-image.outputs.metadata).regular.tags, ' ') }} ``` @@ -434,11 +717,12 @@ jobs: - Digest: ``` - ${{ env.IMAGE }}@${{ fromJSON(needs.build-image.outputs.metadata).debug.digest }} + ghcr.io/element-hq/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).debug.digest }} + oci.element.io/matrix-authentication-service@${{ fromJSON(needs.finalize-image.outputs.metadata).debug.digest }} ``` - Tags: ``` - ${{ join(fromJSON(needs.build-image.outputs.metadata).debug.tags, ' + ${{ join(fromJSON(needs.finalize-image.outputs.metadata).debug.tags, ' ') }} ``` @@ -454,7 +738,7 @@ jobs: if: github.event_name == 'pull_request' && github.event.label.name == 'Z-Build-Workflow' needs: - - build-image + - finalize-image permissions: contents: read @@ -471,7 +755,7 @@ jobs: - name: Remove label and comment uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: - BUILD_IMAGE_MANIFEST: ${{ needs.build-image.outputs.metadata }} + BUILD_IMAGE_MANIFEST: ${{ needs.finalize-image.outputs.metadata }} with: script: | const script = require('./.github/scripts/cleanup-pr.cjs'); diff --git a/Cargo.lock b/Cargo.lock index 575a51699..5ca447c0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3092,7 +3092,7 @@ dependencies = [ [[package]] name = "mas-axum-utils" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "axum", @@ -3126,7 +3126,7 @@ dependencies = [ [[package]] name = "mas-cli" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "axum", @@ -3201,7 +3201,7 @@ dependencies = [ [[package]] name = "mas-config" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "camino", @@ -3232,7 +3232,7 @@ dependencies = [ [[package]] name = "mas-context" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "console", "opentelemetry", @@ -3248,7 +3248,7 @@ dependencies = [ [[package]] name = "mas-data-model" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "base64ct", "chrono", @@ -3270,7 +3270,7 @@ dependencies = [ [[package]] name = "mas-email" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "async-trait", "lettre", @@ -3281,7 +3281,7 @@ dependencies = [ [[package]] name = "mas-handlers" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "aide", "anyhow", @@ -3362,7 +3362,7 @@ dependencies = [ [[package]] name = "mas-http" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "futures-util", "headers", @@ -3383,7 +3383,7 @@ dependencies = [ [[package]] name = "mas-i18n" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "camino", "icu_calendar", @@ -3405,7 +3405,7 @@ dependencies = [ [[package]] name = "mas-i18n-scan" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "camino", "clap", @@ -3419,7 +3419,7 @@ dependencies = [ [[package]] name = "mas-iana" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "schemars 0.9.0", "serde", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "mas-iana-codegen" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "async-trait", @@ -3444,7 +3444,7 @@ dependencies = [ [[package]] name = "mas-jose" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "base64ct", "chrono", @@ -3474,7 +3474,7 @@ dependencies = [ [[package]] name = "mas-keystore" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "aead", "base64ct", @@ -3502,7 +3502,7 @@ dependencies = [ [[package]] name = "mas-listener" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "bytes", @@ -3526,7 +3526,7 @@ dependencies = [ [[package]] name = "mas-matrix" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "async-trait", @@ -3536,7 +3536,7 @@ dependencies = [ [[package]] name = "mas-matrix-synapse" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "async-trait", @@ -3553,7 +3553,7 @@ dependencies = [ [[package]] name = "mas-oidc-client" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "assert_matches", "async-trait", @@ -3589,7 +3589,7 @@ dependencies = [ [[package]] name = "mas-policy" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "arc-swap", @@ -3606,7 +3606,7 @@ dependencies = [ [[package]] name = "mas-router" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "axum", "serde", @@ -3617,7 +3617,7 @@ dependencies = [ [[package]] name = "mas-spa" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "camino", "serde", @@ -3626,7 +3626,7 @@ dependencies = [ [[package]] name = "mas-storage" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "async-trait", "chrono", @@ -3648,7 +3648,7 @@ dependencies = [ [[package]] name = "mas-storage-pg" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "async-trait", "chrono", @@ -3678,7 +3678,7 @@ dependencies = [ [[package]] name = "mas-tasks" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "async-trait", @@ -3710,7 +3710,7 @@ dependencies = [ [[package]] name = "mas-templates" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "arc-swap", @@ -3742,7 +3742,7 @@ dependencies = [ [[package]] name = "mas-tower" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "http", "opentelemetry", @@ -3995,7 +3995,7 @@ dependencies = [ [[package]] name = "oauth2-types" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "assert_matches", "base64ct", @@ -6094,7 +6094,7 @@ dependencies = [ [[package]] name = "syn2mas" -version = "1.17.0-rc.0" +version = "1.17.0" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index a095184c4..5348f2b47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = ["crates/*"] resolver = "2" # Updated in the CI with a `sed` command -package.version = "1.17.0-rc.0" +package.version = "1.17.0" package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial" package.authors = ["Element Backend Team"] package.edition = "2024" @@ -39,35 +39,35 @@ broken_intra_doc_links = "deny" [workspace.dependencies] # Workspace crates -mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.17.0-rc.0" } -mas-cli = { path = "./crates/cli/", version = "=1.17.0-rc.0" } -mas-config = { path = "./crates/config/", version = "=1.17.0-rc.0" } -mas-context = { path = "./crates/context/", version = "=1.17.0-rc.0" } -mas-data-model = { path = "./crates/data-model/", version = "=1.17.0-rc.0" } -mas-email = { path = "./crates/email/", version = "=1.17.0-rc.0" } -mas-graphql = { path = "./crates/graphql/", version = "=1.17.0-rc.0" } -mas-handlers = { path = "./crates/handlers/", version = "=1.17.0-rc.0" } -mas-http = { path = "./crates/http/", version = "=1.17.0-rc.0" } -mas-i18n = { path = "./crates/i18n/", version = "=1.17.0-rc.0" } -mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.17.0-rc.0" } -mas-iana = { path = "./crates/iana/", version = "=1.17.0-rc.0" } -mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.17.0-rc.0" } -mas-jose = { path = "./crates/jose/", version = "=1.17.0-rc.0" } -mas-keystore = { path = "./crates/keystore/", version = "=1.17.0-rc.0" } -mas-listener = { path = "./crates/listener/", version = "=1.17.0-rc.0" } -mas-matrix = { path = "./crates/matrix/", version = "=1.17.0-rc.0" } -mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.17.0-rc.0" } -mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.17.0-rc.0" } -mas-policy = { path = "./crates/policy/", version = "=1.17.0-rc.0" } -mas-router = { path = "./crates/router/", version = "=1.17.0-rc.0" } -mas-spa = { path = "./crates/spa/", version = "=1.17.0-rc.0" } -mas-storage = { path = "./crates/storage/", version = "=1.17.0-rc.0" } -mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.17.0-rc.0" } -mas-tasks = { path = "./crates/tasks/", version = "=1.17.0-rc.0" } -mas-templates = { path = "./crates/templates/", version = "=1.17.0-rc.0" } -mas-tower = { path = "./crates/tower/", version = "=1.17.0-rc.0" } -oauth2-types = { path = "./crates/oauth2-types/", version = "=1.17.0-rc.0" } -syn2mas = { path = "./crates/syn2mas", version = "=1.17.0-rc.0" } +mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.17.0" } +mas-cli = { path = "./crates/cli/", version = "=1.17.0" } +mas-config = { path = "./crates/config/", version = "=1.17.0" } +mas-context = { path = "./crates/context/", version = "=1.17.0" } +mas-data-model = { path = "./crates/data-model/", version = "=1.17.0" } +mas-email = { path = "./crates/email/", version = "=1.17.0" } +mas-graphql = { path = "./crates/graphql/", version = "=1.17.0" } +mas-handlers = { path = "./crates/handlers/", version = "=1.17.0" } +mas-http = { path = "./crates/http/", version = "=1.17.0" } +mas-i18n = { path = "./crates/i18n/", version = "=1.17.0" } +mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.17.0" } +mas-iana = { path = "./crates/iana/", version = "=1.17.0" } +mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.17.0" } +mas-jose = { path = "./crates/jose/", version = "=1.17.0" } +mas-keystore = { path = "./crates/keystore/", version = "=1.17.0" } +mas-listener = { path = "./crates/listener/", version = "=1.17.0" } +mas-matrix = { path = "./crates/matrix/", version = "=1.17.0" } +mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.17.0" } +mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.17.0" } +mas-policy = { path = "./crates/policy/", version = "=1.17.0" } +mas-router = { path = "./crates/router/", version = "=1.17.0" } +mas-spa = { path = "./crates/spa/", version = "=1.17.0" } +mas-storage = { path = "./crates/storage/", version = "=1.17.0" } +mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.17.0" } +mas-tasks = { path = "./crates/tasks/", version = "=1.17.0" } +mas-templates = { path = "./crates/templates/", version = "=1.17.0" } +mas-tower = { path = "./crates/tower/", version = "=1.17.0" } +oauth2-types = { path = "./crates/oauth2-types/", version = "=1.17.0" } +syn2mas = { path = "./crates/syn2mas", version = "=1.17.0" } # OpenAPI schema generation and validation [workspace.dependencies.aide] diff --git a/Dockerfile b/Dockerfile index ba2ed16c8..1998779da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,9 @@ # Please see LICENSE files in the repository root for full details. # Builds a minimal image with the binary only. It is multi-arch capable, -# cross-building to aarch64 and x86_64. When cross-compiling, Docker sets two +# cross-building to aarch64 or x86_64. When cross-compiling, Docker sets two # implicit BUILDARG: BUILDPLATFORM being the host platform and TARGETPLATFORM -# being the platform being built. +# being the platform being built. Each architecture is built separately. # The Debian version and version name must be in sync ARG DEBIAN_VERSION=13 @@ -119,20 +119,25 @@ ENV SQLX_OFFLINE=true ARG VERGEN_GIT_DESCRIBE ENV VERGEN_GIT_DESCRIBE=${VERGEN_GIT_DESCRIBE} +ARG TARGETARCH + # Network access: cargo auditable needs it RUN --network=default \ --mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/app/target \ + RUST_TARGET=$(case "${TARGETARCH}" in \ + amd64) echo "x86_64-unknown-linux-gnu" ;; \ + arm64) echo "aarch64-unknown-linux-gnu" ;; \ + *) echo "unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac) && \ cargo auditable build \ --locked \ --release \ --bin mas-cli \ --no-default-features \ --features docker \ - --target x86_64-unknown-linux-gnu \ - --target aarch64-unknown-linux-gnu \ - && mv "target/x86_64-unknown-linux-gnu/release/mas-cli" /usr/local/bin/mas-cli-amd64 \ - && mv "target/aarch64-unknown-linux-gnu/release/mas-cli" /usr/local/bin/mas-cli-arm64 + --target "${RUST_TARGET}" \ + && mv "target/${RUST_TARGET}/release/mas-cli" /usr/local/bin/mas-cli ####################################### ## Prepare /usr/local/share/mas-cli/ ## @@ -149,8 +154,7 @@ COPY ./translations/ /share/translations ################################## FROM gcr.io/distroless/cc-debian${DEBIAN_VERSION}:debug-nonroot AS debug -ARG TARGETARCH -COPY --from=builder /usr/local/bin/mas-cli-${TARGETARCH} /usr/local/bin/mas-cli +COPY --from=builder /usr/local/bin/mas-cli /usr/local/bin/mas-cli COPY --from=share /share /usr/local/share/mas-cli WORKDIR / @@ -161,8 +165,7 @@ ENTRYPOINT ["/usr/local/bin/mas-cli"] ################### FROM gcr.io/distroless/cc-debian${DEBIAN_VERSION}:nonroot -ARG TARGETARCH -COPY --from=builder /usr/local/bin/mas-cli-${TARGETARCH} /usr/local/bin/mas-cli +COPY --from=builder /usr/local/bin/mas-cli /usr/local/bin/mas-cli COPY --from=share /share /usr/local/share/mas-cli WORKDIR / diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index d4c22b550..ce9a75b91 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -457,6 +457,17 @@ pub(crate) async fn post( // Now we can create the device on the homeserver, without holding the // transaction + // + // Normally, devices get synced to the homeserver in a `SyncDevicesJob` but we + // want the device to be created synchronously on the homeserver, so that + // when we respond, the access token works completely. If the device doesn't + // exist on the homeserver side, token introspection from Synapse to MAS + // will work but Synapse will return a 401 because it doesn't see the + // device. + // + // We're using an upsert so if the device already exists for some reason (like + // when we're replacing it, or a concurrent device sync happening) it won't + // have any effect. if let Err(err) = homeserver .upsert_device( &user.username, @@ -499,28 +510,33 @@ pub(crate) async fn post( })) } -/// Given the violations from [`Policy::evaluate_compat_login`], return the -/// appropriate `RouteError` response. +/// Given the evaluation result/violations from +/// [`Policy::evaluate_compat_login`], return the appropriate `RouteError` +/// response. async fn process_violations_for_compat_login( + rng: &mut (dyn RngCore + Send), clock: &dyn Clock, repo: &mut BoxRepository, session_limit_config: Option<&SessionLimitConfig>, user: &User, - violations: Vec, + res: mas_policy::EvaluationResult, ) -> Result<(), RouteError> { // We're using slice syntax here so we can match easily - match &violations[..] { + match (res.valid(), &res.violations[..]) { // If the only violation is having reached the session limit, we might be // able to resolve the situation. // // We don't trigger this if there was some other violation anyway, since // that means that removing a session wouldn't actually unblock the login. - [ - Violation { - variant: Some(ViolationVariant::TooManySessions { need_to_remove }), - .. - }, - ] => { + ( + false, + [ + Violation { + variant: Some(ViolationVariant::TooManySessions { need_to_remove }), + .. + }, + ], + ) => { // Normally, if we are seeing a `TooManySessions` violation, we would // expect `session_limit_config` to be filled in but if someone created // their own policies which emit a `TooManySessions` violation that isn't @@ -596,6 +612,11 @@ async fn process_violations_for_compat_login( .finish(clock, compat_session.to_owned()) .await?; } + + // Schedule a device sync with the homeserver + repo.queue_job() + .schedule_job(rng, clock, SyncDevicesJob::new_for_id(user.id)) + .await?; } else { // Tell the user about the limit return Err(RouteError::PolicyHardSessionLimitReached); @@ -613,9 +634,9 @@ async fn process_violations_for_compat_login( } } // Nothing is wrong - [] => return Ok(()), + (true, _) => return Ok(()), // Just throw an error for any other violation - _violations => { + (false, _violations) => { // FIXME: We should be exposing the violations to the user return Err(RouteError::PolicyRejected); } @@ -840,11 +861,12 @@ async fn token_login( }) .await?; process_violations_for_compat_login( + rng, clock, repo, session_limit_config, &browser_session.user, - res.violations, + res, ) .await?; @@ -966,8 +988,7 @@ async fn user_password_login( requester: policy_requester, }) .await?; - process_violations_for_compat_login(clock, repo, session_limit_config, &user, res.violations) - .await?; + process_violations_for_compat_login(rng, clock, repo, session_limit_config, &user, res).await?; let session = repo .compat_session() diff --git a/crates/handlers/src/compat/logout.rs b/crates/handlers/src/compat/logout.rs index 4642cc54b..8800fcf44 100644 --- a/crates/handlers/src/compat/logout.rs +++ b/crates/handlers/src/compat/logout.rs @@ -117,13 +117,21 @@ pub(crate) async fn post( // XXX: this is probably not the right error .ok_or(RouteError::InvalidAuthorization)?; + // This will make the access token invalid + repo.compat_session().finish(&clock, session).await?; + // Schedule a job to sync the devices of the user with the homeserver + // + // Doing this in a background job is ok as the access token will be invalid + // right away (from the session being finished above) and we do actually + // want to do a full device list sync (as opposed to + // `homeserver.delete_device(...)`), because we're not sure whether we want + // to delete the device (if there is for example a concurrent logout and + // login with the same device ID). repo.queue_job() .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user)) .await?; - repo.compat_session().finish(&clock, session).await?; - repo.save().await?; LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "success")]); diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index e070b640b..925f4209d 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -573,6 +573,15 @@ async fn authorization_code_grant( // Look for device to provision for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { + // Normally, devices get synced to the homeserver in a `SyncDevicesJob` but we + // want the device to be created synchronously on the homeserver, so + // that when we respond, the access token works completely. If the + // device doesn't exist on the homeserver side, token introspection + // from Synapse to MAS will work but Synapse will return a 401 + // because it doesn't see the device. + // + // We're using an upsert so if the device already exists for some reason + // (like when a concurrent device sync happening) it won't have any effect. homeserver .upsert_device( &browser_session.user.username, @@ -977,6 +986,15 @@ async fn device_code_grant( // Look for device to provision for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { + // Normally, devices get synced to the homeserver in a `SyncDevicesJob` but we + // want the device to be created synchronously on the homeserver, so + // that when we respond, the access token works completely. If the + // device doesn't exist on the homeserver side, token introspection + // from Synapse to MAS will work but Synapse will return a 401 + // because it doesn't see the device. + // + // We're using an upsert so if the device already exists for some reason + // (like when a concurrent device sync happening) it won't have any effect. homeserver .upsert_device(&browser_session.user.username, device.as_str(), None) .await diff --git a/docker-bake.hcl b/docker-bake.hcl index 3c3cac3af..6f6879e1a 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,3 +1,4 @@ +# Copyright 2025, 2026 Element Creations Ltd. # Copyright 2025 New Vector Ltd. # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial @@ -15,8 +16,8 @@ group "default" { targets = ["regular", "debug"] } target "docker-metadata-action" {} target "docker-metadata-action-debug" {} -// This sets the platforms and is further extended by GitHub Actions to set the -// output and the cache locations +// This is extended by GitHub Actions to set the output, cache locations, +// and platforms (one architecture per job for parallel builds) target "base" { args = { // This is set so that when we use a git context, the .git directory is @@ -26,11 +27,6 @@ target "base" { // Pass down the version from an external git describe source VERGEN_GIT_DESCRIBE = "${VERGEN_GIT_DESCRIBE}" } - - platforms = [ - "linux/amd64", - "linux/arm64", - ] } target "regular" { diff --git a/frontend/.storybook/locales.ts b/frontend/.storybook/locales.ts index ffcc627ed..bb379022a 100644 --- a/frontend/.storybook/locales.ts +++ b/frontend/.storybook/locales.ts @@ -27,7 +27,7 @@ export type LocalazyMetadata = { }; const localazyMetadata: LocalazyMetadata = { - projectUrl: "https://localazy.com/p/matrix-authentication-service", + projectUrl: "https://localazy.com/p/matrix-authentication-service!v1.17", baseLocale: "en", languages: [ { @@ -208,25 +208,25 @@ const localazyMetadata: LocalazyMetadata = { file: "frontend.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", - "hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", - "nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", - "pl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", - "pt_BR": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt-BR/frontend.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", - "sk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sk/frontend.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", - "uz": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uz/frontend.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" + "cs": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", + "da": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", + "de": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", + "en": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", + "et": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", + "fi": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", + "fr": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", + "hu": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", + "nb_NO": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", + "nl": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", + "pl": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json", + "pt": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", + "pt_BR": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt-BR/frontend.json", + "ru": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", + "sk": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sk/frontend.json", + "sv": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", + "uk": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", + "uz": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uz/frontend.json", + "zh#Hans": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" } }, { @@ -234,25 +234,25 @@ const localazyMetadata: LocalazyMetadata = { file: "file.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", - "hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", - "nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", - "pl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", - "pt_BR": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt-BR/file.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", - "sk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sk/file.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", - "uz": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uz/file.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" + "cs": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", + "da": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", + "de": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", + "en": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", + "et": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", + "fi": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", + "fr": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", + "hu": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", + "nb_NO": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", + "nl": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", + "pl": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json", + "pt": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", + "pt_BR": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt-BR/file.json", + "ru": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", + "sk": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sk/file.json", + "sv": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", + "uk": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", + "uz": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uz/file.json", + "zh#Hans": "https://delivery.localazy.com/_a65347067752521597762935bd1b/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" } } ]