diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 548e53eca..b7be8b33d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: # Need a full clone so that `git describe` reports the right version fetch-depth: 0 @@ -67,7 +67,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - uses: ./.github/actions/build-frontend - uses: ./.github/actions/build-policies @@ -112,7 +112,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -226,7 +226,7 @@ jobs: steps: - name: Docker meta id: meta - uses: docker/metadata-action@v5.9.0 + uses: docker/metadata-action@v5.10.0 with: images: "${{ env.IMAGE }}" bake-target: docker-metadata-action @@ -242,7 +242,7 @@ jobs: - name: Docker meta (debug variant) id: meta-debug - uses: docker/metadata-action@v5.9.0 + uses: docker/metadata-action@v5.10.0 with: images: "${{ env.IMAGE }}" bake-target: docker-metadata-action-debug @@ -276,7 +276,7 @@ jobs: - name: Build and push id: bake - uses: docker/bake-action@v6.9.0 + uses: docker/bake-action@v6.10.0 with: files: | ./docker-bake.hcl @@ -327,7 +327,7 @@ jobs: merge-multiple: true - name: Prepare a release - uses: softprops/action-gh-release@v2.4.2 + uses: softprops/action-gh-release@v2.5.0 with: generate_release_notes: true body: | @@ -376,7 +376,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: sparse-checkout: | .github/scripts @@ -396,7 +396,7 @@ jobs: await script({ core, github, context }); - name: Update unstable release - uses: softprops/action-gh-release@v2.4.2 + uses: softprops/action-gh-release@v2.5.0 with: name: "Unstable build" tag_name: unstable @@ -454,7 +454,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: sparse-checkout: | .github/scripts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ed119c2e3..bf7ac011a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - uses: ./.github/actions/build-policies @@ -61,10 +61,10 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 24 @@ -85,10 +85,10 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 24 @@ -109,10 +109,10 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 24 @@ -133,7 +133,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@nightly @@ -156,10 +156,10 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Run `cargo-deny` - uses: EmbarkStudios/cargo-deny-action@v2.0.13 + uses: EmbarkStudios/cargo-deny-action@v2.0.14 with: rust-version: stable @@ -172,7 +172,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain run: | @@ -213,7 +213,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.89.0 @@ -238,7 +238,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -291,7 +291,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 3046f45cc..74eeef3b4 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - uses: ./.github/actions/build-policies @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - uses: ./.github/actions/build-frontend env: @@ -99,7 +99,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 84074d85e..3053b56fb 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -39,7 +39,7 @@ jobs: tool: mdbook - name: Install Node - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 24 diff --git a/.github/workflows/merge-back.yaml b/.github/workflows/merge-back.yaml index d28d68c20..8239442fc 100644 --- a/.github/workflows/merge-back.yaml +++ b/.github/workflows/merge-back.yaml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: sparse-checkout: | .github/scripts diff --git a/.github/workflows/release-branch.yaml b/.github/workflows/release-branch.yaml index e31fa2c82..e058a62be 100644 --- a/.github/workflows/release-branch.yaml +++ b/.github/workflows/release-branch.yaml @@ -34,7 +34,7 @@ jobs: run: exit 1 - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -61,10 +61,10 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 24 @@ -106,7 +106,7 @@ jobs: needs: [tag, compute-version, localazy] steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: sparse-checkout: | .github/scripts diff --git a/.github/workflows/release-bump.yaml b/.github/workflows/release-bump.yaml index 46251a410..a2a20791a 100644 --- a/.github/workflows/release-bump.yaml +++ b/.github/workflows/release-bump.yaml @@ -33,7 +33,7 @@ jobs: run: exit 1 - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -76,7 +76,7 @@ jobs: needs: [tag, compute-version] steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: sparse-checkout: | .github/scripts diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index ec3ea290d..c6c394c81 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/translations-download.yaml b/.github/workflows/translations-download.yaml index a022203fc..70419fce0 100644 --- a/.github/workflows/translations-download.yaml +++ b/.github/workflows/translations-download.yaml @@ -19,10 +19,10 @@ jobs: run: exit 1 - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 24 @@ -42,7 +42,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v7.0.8 + uses: peter-evans/create-pull-request@v8.0.0 with: sign-commits: true token: ${{ secrets.BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/translations-upload.yaml b/.github/workflows/translations-upload.yaml index 063a228dd..ba723278e 100644 --- a/.github/workflows/translations-upload.yaml +++ b/.github/workflows/translations-upload.yaml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Node - uses: actions/setup-node@v6.0.0 + uses: actions/setup-node@v6.1.0 with: node-version: 24 diff --git a/Cargo.lock b/Cargo.lock index 1842a3e75..bfc949f41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,9 +1089,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "convert_case" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190" dependencies = [ "unicode-segmentation", ] @@ -3097,7 +3097,7 @@ dependencies = [ [[package]] name = "mas-axum-utils" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "axum", @@ -3131,7 +3131,7 @@ dependencies = [ [[package]] name = "mas-cli" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "axum", @@ -3204,7 +3204,7 @@ dependencies = [ [[package]] name = "mas-config" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "camino", @@ -3236,7 +3236,7 @@ dependencies = [ [[package]] name = "mas-context" -version = "1.7.0" +version = "1.8.0" dependencies = [ "console", "opentelemetry", @@ -3252,7 +3252,7 @@ dependencies = [ [[package]] name = "mas-data-model" -version = "1.7.0" +version = "1.8.0" dependencies = [ "base64ct", "chrono", @@ -3275,7 +3275,7 @@ dependencies = [ [[package]] name = "mas-email" -version = "1.7.0" +version = "1.8.0" dependencies = [ "async-trait", "lettre", @@ -3286,7 +3286,7 @@ dependencies = [ [[package]] name = "mas-handlers" -version = "1.7.0" +version = "1.8.0" dependencies = [ "aide", "anyhow", @@ -3366,7 +3366,7 @@ dependencies = [ [[package]] name = "mas-http" -version = "1.7.0" +version = "1.8.0" dependencies = [ "futures-util", "headers", @@ -3387,7 +3387,7 @@ dependencies = [ [[package]] name = "mas-i18n" -version = "1.7.0" +version = "1.8.0" dependencies = [ "camino", "icu_calendar", @@ -3409,7 +3409,7 @@ dependencies = [ [[package]] name = "mas-i18n-scan" -version = "1.7.0" +version = "1.8.0" dependencies = [ "camino", "clap", @@ -3423,7 +3423,7 @@ dependencies = [ [[package]] name = "mas-iana" -version = "1.7.0" +version = "1.8.0" dependencies = [ "schemars 0.9.0", "serde", @@ -3431,7 +3431,7 @@ dependencies = [ [[package]] name = "mas-iana-codegen" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "async-trait", @@ -3448,7 +3448,7 @@ dependencies = [ [[package]] name = "mas-jose" -version = "1.7.0" +version = "1.8.0" dependencies = [ "base64ct", "chrono", @@ -3478,7 +3478,7 @@ dependencies = [ [[package]] name = "mas-keystore" -version = "1.7.0" +version = "1.8.0" dependencies = [ "aead", "base64ct", @@ -3506,7 +3506,7 @@ dependencies = [ [[package]] name = "mas-listener" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "bytes", @@ -3531,7 +3531,7 @@ dependencies = [ [[package]] name = "mas-matrix" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "async-trait", @@ -3541,7 +3541,7 @@ dependencies = [ [[package]] name = "mas-matrix-synapse" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "async-trait", @@ -3558,7 +3558,7 @@ dependencies = [ [[package]] name = "mas-oidc-client" -version = "1.7.0" +version = "1.8.0" dependencies = [ "assert_matches", "async-trait", @@ -3594,7 +3594,7 @@ dependencies = [ [[package]] name = "mas-policy" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "arc-swap", @@ -3611,7 +3611,7 @@ dependencies = [ [[package]] name = "mas-router" -version = "1.7.0" +version = "1.8.0" dependencies = [ "axum", "serde", @@ -3622,7 +3622,7 @@ dependencies = [ [[package]] name = "mas-spa" -version = "1.7.0" +version = "1.8.0" dependencies = [ "camino", "serde", @@ -3631,7 +3631,7 @@ dependencies = [ [[package]] name = "mas-storage" -version = "1.7.0" +version = "1.8.0" dependencies = [ "async-trait", "chrono", @@ -3653,10 +3653,11 @@ dependencies = [ [[package]] name = "mas-storage-pg" -version = "1.7.0" +version = "1.8.0" dependencies = [ "async-trait", "chrono", + "crc", "futures-util", "mas-data-model", "mas-iana", @@ -3673,6 +3674,7 @@ dependencies = [ "sha2", "sqlx", "thiserror 2.0.17", + "tokio", "tracing", "ulid", "url", @@ -3681,7 +3683,7 @@ dependencies = [ [[package]] name = "mas-tasks" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "async-trait", @@ -3713,7 +3715,7 @@ dependencies = [ [[package]] name = "mas-templates" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "arc-swap", @@ -3723,6 +3725,7 @@ dependencies = [ "mas-data-model", "mas-i18n", "mas-iana", + "mas-policy", "mas-router", "mas-spa", "minijinja", @@ -3744,7 +3747,7 @@ dependencies = [ [[package]] name = "mas-tower" -version = "1.7.0" +version = "1.8.0" dependencies = [ "http", "opentelemetry", @@ -4014,7 +4017,7 @@ dependencies = [ [[package]] name = "oauth2-types" -version = "1.7.0" +version = "1.8.0" dependencies = [ "assert_matches", "base64ct", @@ -5383,9 +5386,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "sentry" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20" +checksum = "d9794f69ad475e76c057e326175d3088509649e3aed98473106b9fe94ba59424" dependencies = [ "httpdate", "reqwest", @@ -5400,9 +5403,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3253a495ab536f6de1746a58d5d7824b77d75e08e1a4b8ca6fb356839077ae0" +checksum = "e81137ad53b8592bd0935459ad74c0376053c40084aa170451e74eeea8dbc6c3" dependencies = [ "backtrace", "regex", @@ -5411,9 +5414,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027f81a728836e66b88c07666a10f5ed5a35e2695b04eb7aa0fcbed93f814900" +checksum = "cfb403c66cc2651a01b9bacda2e7c22cd51f7e8f56f206aa4310147eb3259282" dependencies = [ "hostname", "libc", @@ -5425,9 +5428,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b6729c8e71ac968edbe9bf2dd4109c162e552b52bacd2b07e24ede1aba84a5" +checksum = "cfc409727ae90765ca8ea76fe6c949d6f159a11d02e130b357fa652ee9efcada" dependencies = [ "rand 0.9.2", "sentry-types", @@ -5438,9 +5441,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac0471f04f8f97af0c17eeca2c516e23faa1c0271a55bc64371d9ce488c2d40" +checksum = "3df79f4e1e72b2a8b75a0ebf49e78709ceb9b3f0b451f13adc92a0361b0aaabe" dependencies = [ "sentry-backtrace", "sentry-core", @@ -5448,9 +5451,9 @@ dependencies = [ [[package]] name = "sentry-tower" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417bd48071863a65ca5f33d15af9aabd49a5cee7f97415d3f08ce8c90ed2c531" +checksum = "7eec9885bceb8ba374858d015bb6fa39dbb341d94ca088bc8f13bee2e64e2c68" dependencies = [ "axum", "http", @@ -5463,9 +5466,9 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428f780866a613142dcc81b7f8551ae4d1c056f4df22b6d7ddd9154a9974eb03" +checksum = "ff2046f527fd4b75e0b6ab3bd656c67dce42072f828dc4d03c206d15dca74a93" dependencies = [ "bitflags", "sentry-backtrace", @@ -5476,9 +5479,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c19d1d1967b55659c358886d0f1aa3076488d445f84c7d727d384c675adaec1" +checksum = "c7b9b4e4c03a4d3643c18c78b8aa91d2cbee5da047d2fa0ca4bb29bc67e6c55c" dependencies = [ "debugid", "hex", @@ -6085,7 +6088,7 @@ dependencies = [ [[package]] name = "syn2mas" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "arc-swap", @@ -6508,12 +6511,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index deafed604..7b1aba0d4 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.7.0" +package.version = "1.8.0" package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial" package.authors = ["Element Backend Team"] package.edition = "2024" @@ -34,35 +34,35 @@ broken_intra_doc_links = "deny" [workspace.dependencies] # Workspace crates -mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.7.0" } -mas-cli = { path = "./crates/cli/", version = "=1.7.0" } -mas-config = { path = "./crates/config/", version = "=1.7.0" } -mas-context = { path = "./crates/context/", version = "=1.7.0" } -mas-data-model = { path = "./crates/data-model/", version = "=1.7.0" } -mas-email = { path = "./crates/email/", version = "=1.7.0" } -mas-graphql = { path = "./crates/graphql/", version = "=1.7.0" } -mas-handlers = { path = "./crates/handlers/", version = "=1.7.0" } -mas-http = { path = "./crates/http/", version = "=1.7.0" } -mas-i18n = { path = "./crates/i18n/", version = "=1.7.0" } -mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.7.0" } -mas-iana = { path = "./crates/iana/", version = "=1.7.0" } -mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.7.0" } -mas-jose = { path = "./crates/jose/", version = "=1.7.0" } -mas-keystore = { path = "./crates/keystore/", version = "=1.7.0" } -mas-listener = { path = "./crates/listener/", version = "=1.7.0" } -mas-matrix = { path = "./crates/matrix/", version = "=1.7.0" } -mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.7.0" } -mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.7.0" } -mas-policy = { path = "./crates/policy/", version = "=1.7.0" } -mas-router = { path = "./crates/router/", version = "=1.7.0" } -mas-spa = { path = "./crates/spa/", version = "=1.7.0" } -mas-storage = { path = "./crates/storage/", version = "=1.7.0" } -mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.7.0" } -mas-tasks = { path = "./crates/tasks/", version = "=1.7.0" } -mas-templates = { path = "./crates/templates/", version = "=1.7.0" } -mas-tower = { path = "./crates/tower/", version = "=1.7.0" } -oauth2-types = { path = "./crates/oauth2-types/", version = "=1.7.0" } -syn2mas = { path = "./crates/syn2mas", version = "=1.7.0" } +mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.8.0" } +mas-cli = { path = "./crates/cli/", version = "=1.8.0" } +mas-config = { path = "./crates/config/", version = "=1.8.0" } +mas-context = { path = "./crates/context/", version = "=1.8.0" } +mas-data-model = { path = "./crates/data-model/", version = "=1.8.0" } +mas-email = { path = "./crates/email/", version = "=1.8.0" } +mas-graphql = { path = "./crates/graphql/", version = "=1.8.0" } +mas-handlers = { path = "./crates/handlers/", version = "=1.8.0" } +mas-http = { path = "./crates/http/", version = "=1.8.0" } +mas-i18n = { path = "./crates/i18n/", version = "=1.8.0" } +mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.8.0" } +mas-iana = { path = "./crates/iana/", version = "=1.8.0" } +mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.8.0" } +mas-jose = { path = "./crates/jose/", version = "=1.8.0" } +mas-keystore = { path = "./crates/keystore/", version = "=1.8.0" } +mas-listener = { path = "./crates/listener/", version = "=1.8.0" } +mas-matrix = { path = "./crates/matrix/", version = "=1.8.0" } +mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.8.0" } +mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.8.0" } +mas-policy = { path = "./crates/policy/", version = "=1.8.0" } +mas-router = { path = "./crates/router/", version = "=1.8.0" } +mas-spa = { path = "./crates/spa/", version = "=1.8.0" } +mas-storage = { path = "./crates/storage/", version = "=1.8.0" } +mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.8.0" } +mas-tasks = { path = "./crates/tasks/", version = "=1.8.0" } +mas-templates = { path = "./crates/templates/", version = "=1.8.0" } +mas-tower = { path = "./crates/tower/", version = "=1.8.0" } +oauth2-types = { path = "./crates/oauth2-types/", version = "=1.8.0" } +syn2mas = { path = "./crates/syn2mas", version = "=1.8.0" } # OpenAPI schema generation and validation [workspace.dependencies.aide] @@ -177,7 +177,7 @@ features = ["std"] # Utility for converting between different cases [workspace.dependencies.convert_case] -version = "0.8.0" +version = "0.9.0" # CRC calculation [workspace.dependencies.crc] @@ -567,18 +567,18 @@ features = [ # Sentry error tracking [workspace.dependencies.sentry] -version = "0.45.0" +version = "0.46.0" default-features = false features = ["backtrace", "contexts", "panic", "tower", "reqwest"] # Sentry tower layer [workspace.dependencies.sentry-tower] -version = "0.45.0" +version = "0.46.0" features = ["http", "axum-matched-path"] # Sentry tracing integration [workspace.dependencies.sentry-tracing] -version = "0.45.0" +version = "0.46.0" # Serialization and deserialization [workspace.dependencies.serde] @@ -688,7 +688,7 @@ version = "0.1.41" version = "0.3.20" features = ["env-filter"] [workspace.dependencies.tracing-appender] -version = "0.2.3" +version = "0.2.4" # URL manipulation [workspace.dependencies.url] diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index 034f84b4a..ae4d329ff 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -12,10 +12,9 @@ use clap::Parser; use figment::Figment; use mas_config::{ConfigurationSection, RootConfig, SyncConfig}; use mas_data_model::{Clock as _, SystemClock}; -use mas_storage_pg::MIGRATOR; use rand::SeedableRng; use tokio::io::AsyncWriteExt; -use tracing::{Instrument, info, info_span}; +use tracing::{info, info_span}; use crate::util::database_connection_from_config; @@ -129,9 +128,7 @@ impl Options { // Grab a connection to the database let mut conn = database_connection_from_config(&config.database).await?; - MIGRATOR - .run(&mut conn) - .instrument(info_span!("db.migrate")) + mas_storage_pg::migrate(&mut conn) .await .context("could not run migrations")?; diff --git a/crates/cli/src/commands/database.rs b/crates/cli/src/commands/database.rs index 519536fff..7acc6830e 100644 --- a/crates/cli/src/commands/database.rs +++ b/crates/cli/src/commands/database.rs @@ -10,8 +10,7 @@ use anyhow::Context; use clap::Parser; use figment::Figment; use mas_config::{ConfigurationSectionExt, DatabaseConfig}; -use mas_storage_pg::MIGRATOR; -use tracing::{Instrument, info_span}; +use tracing::info_span; use crate::util::database_connection_from_config; @@ -35,9 +34,7 @@ impl Options { let mut conn = database_connection_from_config(&config).await?; // Run pending migrations - MIGRATOR - .run(&mut conn) - .instrument(info_span!("db.migrate")) + mas_storage_pg::migrate(&mut conn) .await .context("could not run migrations")?; diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index d64cd26a3..b72d48111 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. -use std::{collections::BTreeSet, process::ExitCode, sync::Arc, time::Duration}; +use std::{process::ExitCode, sync::Arc, time::Duration}; use anyhow::Context; use clap::Parser; @@ -18,9 +18,8 @@ use mas_data_model::SystemClock; use mas_handlers::{ActivityTracker, CookieManager, Limiter, MetadataCache}; use mas_listener::server::Server; use mas_router::UrlBuilder; -use mas_storage_pg::{MIGRATOR, PgRepositoryFactory}; -use sqlx::migrate::Migrate; -use tracing::{Instrument, info, info_span, warn}; +use mas_storage_pg::PgRepositoryFactory; +use tracing::{info, info_span, warn}; use crate::{ app_state::AppState, @@ -73,24 +72,20 @@ impl Options { let pool = database_pool_from_config(&config.database).await?; if self.no_migrate { - // Check that we applied all the migrations let mut conn = pool.acquire().await?; - let applied = conn.list_applied_migrations().await?; - let applied: BTreeSet<_> = applied.into_iter().map(|m| m.version).collect(); - let has_missing_migrations = MIGRATOR.iter().any(|m| !applied.contains(&m.version)); - if has_missing_migrations { + let pending_migrations = mas_storage_pg::pending_migrations(&mut conn).await?; + if !pending_migrations.is_empty() { // Refuse to start if there are pending migrations return Err(anyhow::anyhow!( - "The server is running with `--no-migrate` but there are pending. Please run them first with `mas-cli database migrate`, or omit the `--no-migrate` flag to apply them automatically on startup." + "The server is running with `--no-migrate` but there are pending migrations. Please run them first with `mas-cli database migrate`, or omit the `--no-migrate` flag to apply them automatically on startup." )); } } else { info!("Running pending database migrations"); - MIGRATOR - .run(&pool) - .instrument(info_span!("db.migrate")) + let mut conn = pool.acquire().await?; + mas_storage_pg::migrate(&mut conn) .await - .context("could not run database migrations")?; + .context("could not run migrations")?; } let encrypter = config.secrets.encrypter().await?; diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index c28935af5..dfa835b95 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -14,13 +14,12 @@ use mas_config::{ UpstreamOAuth2Config, }; use mas_data_model::SystemClock; -use mas_storage_pg::MIGRATOR; use rand::thread_rng; use sqlx::{Connection, Either, PgConnection, postgres::PgConnectOptions, types::Uuid}; use syn2mas::{ LockedMasDatabase, MasWriter, Progress, ProgressStage, SynapseReader, synapse_config, }; -use tracing::{Instrument, error, info, info_span}; +use tracing::{Instrument, error, info}; use crate::util::{DatabaseConnectOptions, database_connection_from_config_with_options}; @@ -122,9 +121,7 @@ impl Options { ) .await?; - MIGRATOR - .run(&mut mas_connection) - .instrument(info_span!("db.migrate")) + mas_storage_pg::migrate(&mut mas_connection) .await .context("could not run migrations")?; diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index 1aa8f7402..c4aeb9a9c 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -45,6 +45,12 @@ fn map_import_on_conflict( mas_config::UpstreamOAuth2OnConflict::Add => { mas_data_model::UpstreamOAuthProviderOnConflict::Add } + mas_config::UpstreamOAuth2OnConflict::Replace => { + mas_data_model::UpstreamOAuthProviderOnConflict::Replace + } + mas_config::UpstreamOAuth2OnConflict::Set => { + mas_data_model::UpstreamOAuthProviderOnConflict::Set + } mas_config::UpstreamOAuth2OnConflict::Fail => { mas_data_model::UpstreamOAuthProviderOnConflict::Fail } @@ -58,6 +64,7 @@ fn map_claims_imports( subject: mas_data_model::UpstreamOAuthProviderSubjectPreference { template: config.subject.template.clone(), }, + skip_confirmation: config.skip_confirmation, localpart: mas_data_model::UpstreamOAuthProviderLocalpartPreference { action: map_import_action(config.localpart.action), template: config.localpart.template.clone(), diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index c0f31557b..454276150 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -145,6 +145,7 @@ pub async fn policy_factory_from_config( register: config.register_entrypoint.clone(), client_registration: config.client_registration_entrypoint.clone(), authorization_grant: config.authorization_grant_entrypoint.clone(), + compat_login: config.compat_login_entrypoint.clone(), email: config.email_entrypoint.clone(), }; diff --git a/crates/config/src/sections/policy.rs b/crates/config/src/sections/policy.rs index 37d052ade..3b816b713 100644 --- a/crates/config/src/sections/policy.rs +++ b/crates/config/src/sections/policy.rs @@ -62,6 +62,14 @@ fn is_default_password_entrypoint(value: &String) -> bool { *value == default_password_entrypoint() } +fn default_compat_login_entrypoint() -> String { + "compat_login/violation".to_owned() +} + +fn is_default_compat_login_entrypoint(value: &String) -> bool { + *value == default_compat_login_entrypoint() +} + fn default_email_entrypoint() -> String { "email/violation".to_owned() } @@ -111,6 +119,13 @@ pub struct PolicyConfig { )] pub authorization_grant_entrypoint: String, + /// Entrypoint to use when evaluating compatibility logins + #[serde( + default = "default_compat_login_entrypoint", + skip_serializing_if = "is_default_compat_login_entrypoint" + )] + pub compat_login_entrypoint: String, + /// Entrypoint to use when changing password #[serde( default = "default_password_entrypoint", @@ -137,6 +152,7 @@ impl Default for PolicyConfig { client_registration_entrypoint: default_client_registration_entrypoint(), register_entrypoint: default_register_entrypoint(), authorization_grant_entrypoint: default_authorization_grant_entrypoint(), + compat_login_entrypoint: default_compat_login_entrypoint(), password_entrypoint: default_password_entrypoint(), email_entrypoint: default_email_entrypoint(), data: default_data(), diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index 53eae7a1b..40591b004 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -118,16 +118,36 @@ impl ConfigurationSection for UpstreamOAuth2Config { } } + if provider.claims_imports.skip_confirmation { + if provider.claims_imports.localpart.action != ImportAction::Require { + return Err(annotate(figment::Error::custom( + "The field `action` must be `require` when `skip_confirmation` is set to `true`", + )).with_path("claims_imports.localpart").into()); + } + + if provider.claims_imports.email.action == ImportAction::Suggest { + return Err(annotate(figment::Error::custom( + "The field `action` must not be `suggest` when `skip_confirmation` is set to `true`", + )).with_path("claims_imports.email").into()); + } + + if provider.claims_imports.displayname.action == ImportAction::Suggest { + return Err(annotate(figment::Error::custom( + "The field `action` must not be `suggest` when `skip_confirmation` is set to `true`", + )).with_path("claims_imports.displayname").into()); + } + } + if matches!( provider.claims_imports.localpart.on_conflict, - OnConflict::Add + OnConflict::Add | OnConflict::Replace | OnConflict::Set ) && !matches!( provider.claims_imports.localpart.action, ImportAction::Force | ImportAction::Require ) { return Err(annotate(figment::Error::custom( - "The field `action` must be either `force` or `require` when `on_conflict` is set to `add`", - )).into()); + "The field `action` must be either `force` or `require` when `on_conflict` is set to `add`, `replace` or `set`", + )).with_path("claims_imports.localpart").into()); } } @@ -206,13 +226,20 @@ impl ImportAction { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum OnConflict { - /// Fails the sso login on conflict + /// Fails the upstream OAuth 2.0 login on conflict #[default] Fail, - /// Adds the oauth identity link, regardless of whether there is an existing - /// link or not + /// Adds the upstream OAuth 2.0 identity link, regardless of whether there + /// is an existing link or not Add, + + /// Replace any existing upstream OAuth 2.0 identity link + Replace, + + /// Adds the upstream OAuth 2.0 identity link *only* if there is no existing + /// link for this provider on the matching user + Set, } impl OnConflict { @@ -326,6 +353,13 @@ pub struct ClaimsImports { #[serde(default, skip_serializing_if = "SubjectImportPreference::is_default")] pub subject: SubjectImportPreference, + /// Whether to skip the interactive screen prompting the user to confirm the + /// attributes that are being imported. This requires `localpart.action` to + /// be `require` and other attribute actions to be either `ignore`, `force` + /// or `require` + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub skip_confirmation: bool, + /// Import the localpart of the MXID #[serde(default, skip_serializing_if = "LocalpartImportPreference::is_default")] pub localpart: LocalpartImportPreference, @@ -337,8 +371,7 @@ pub struct ClaimsImports { )] pub displayname: DisplaynameImportPreference, - /// Import the email address of the user based on the `email` and - /// `email_verified` claims + /// Import the email address of the user #[serde(default, skip_serializing_if = "EmailImportPreference::is_default")] pub email: EmailImportPreference, @@ -354,8 +387,10 @@ impl ClaimsImports { const fn is_default(&self) -> bool { self.subject.is_default() && self.localpart.is_default() + && !self.skip_confirmation && self.displayname.is_default() && self.email.is_default() + && self.account_name.is_default() } } diff --git a/crates/data-model/src/lib.rs b/crates/data-model/src/lib.rs index fd5c0e633..05b2466b9 100644 --- a/crates/data-model/src/lib.rs +++ b/crates/data-model/src/lib.rs @@ -56,8 +56,8 @@ pub use self::{ }, user_agent::{DeviceType, UserAgent}, users::{ - Authentication, AuthenticationMethod, BrowserSession, Password, User, UserEmail, - UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession, + Authentication, AuthenticationMethod, BrowserSession, MatrixUser, Password, User, + UserEmail, UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession, UserRecoveryTicket, UserRegistration, UserRegistrationPassword, UserRegistrationToken, }, utils::{BoxClock, BoxRng}, diff --git a/crates/data-model/src/upstream_oauth2/provider.rs b/crates/data-model/src/upstream_oauth2/provider.rs index be42cb5a5..94f6c2e51 100644 --- a/crates/data-model/src/upstream_oauth2/provider.rs +++ b/crates/data-model/src/upstream_oauth2/provider.rs @@ -312,6 +312,9 @@ pub struct ClaimsImports { #[serde(default)] pub subject: SubjectPreference, + #[serde(default)] + pub skip_confirmation: bool, + #[serde(default)] pub localpart: LocalpartPreference, @@ -415,11 +418,18 @@ impl ImportAction { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum OnConflict { - /// Fails the upstream OAuth 2.0 login + /// Fails the upstream OAuth 2.0 login on conflict #[default] Fail, - /// Adds the upstream account link, regardless of whether there is an - /// existing link or not + /// Adds the upstream OAuth 2.0 identity link, regardless of whether there + /// is an existing link or not Add, + + /// Replace any existing upstream OAuth 2.0 identity link + Replace, + + /// Adds the upstream OAuth 2.0 identity link *only* if there is no existing + /// link for this provider on the matching user + Set, } diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index 541eb26d2..78c483e12 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -12,6 +12,12 @@ use serde::Serialize; use ulid::Ulid; use url::Url; +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct MatrixUser { + pub mxid: String, + pub display_name: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct User { pub id: Ulid, diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index d3c7c979f..9f10d0373 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -16,6 +16,7 @@ use mas_data_model::{ User, }; use mas_matrix::HomeserverConnection; +use mas_policy::{Policy, Requester, ViolationCode, model::CompatLogin}; use mas_storage::{ BoxRepository, BoxRepositoryFactory, RepositoryAccess, compat::{ @@ -37,6 +38,7 @@ use crate::{ BoundActivityTracker, Limiter, METER, RequesterFingerprint, impl_from_error_for_route, passwords::{PasswordManager, PasswordVerificationResult}, rate_limit::PasswordCheckLimitedError, + session::count_user_sessions_for_limiting, }; static LOGIN_COUNTER: LazyLock> = LazyLock::new(|| { @@ -213,9 +215,16 @@ pub enum RouteError { #[error("failed to provision device")] ProvisionDeviceFailed(#[source] anyhow::Error), + + #[error("login rejected by policy")] + PolicyRejected, + + #[error("login rejected by policy (hard session limit reached)")] + PolicyHardSessionLimitReached, } impl_from_error_for_route!(mas_storage::RepositoryError); +impl_from_error_for_route!(mas_policy::EvaluationError); impl From for RouteError { fn from(err: anyhow::Error) -> Self { @@ -274,6 +283,16 @@ impl IntoResponse for RouteError { error: "User account has been locked", status: StatusCode::UNAUTHORIZED, }, + Self::PolicyRejected => MatrixError { + errcode: "M_FORBIDDEN", + error: "Login denied by the policy enforced by this service", + status: StatusCode::FORBIDDEN, + }, + Self::PolicyHardSessionLimitReached => MatrixError { + errcode: "M_FORBIDDEN", + error: "You have reached your hard device limit. Please visit your account page to sign some out.", + status: StatusCode::FORBIDDEN, + }, }; (sentry_event_id, response).into_response() @@ -290,6 +309,7 @@ pub(crate) async fn post( State(homeserver): State>, State(site_config): State, State(limiter): State, + mut policy: Policy, requester: RequesterFingerprint, user_agent: Option>, MatrixJsonBody(input): MatrixJsonBody, @@ -329,6 +349,11 @@ pub(crate) async fn post( &limiter, requester, &mut repo, + &mut policy, + Requester { + ip_address: activity_tracker.ip(), + user_agent: user_agent.clone(), + }, username, password, input.device_id, // TODO check for validity @@ -342,6 +367,11 @@ pub(crate) async fn post( &mut rng, &clock, &mut repo, + &mut policy, + Requester { + ip_address: activity_tracker.ip(), + user_agent: user_agent.clone(), + }, &token, input.device_id, input.initial_device_display_name, @@ -459,6 +489,8 @@ async fn token_login( rng: &mut (dyn RngCore + Send), clock: &dyn Clock, repo: &mut BoxRepository, + policy: &mut Policy, + requester: Requester, token: &str, requested_device_id: Option, initial_device_display_name: Option, @@ -544,10 +576,38 @@ async fn token_login( Device::generate(rng) }; - repo.app_session() + let session_replaced = repo + .app_session() .finish_sessions_to_replace_device(clock, &browser_session.user, &device) .await?; + let session_counts = count_user_sessions_for_limiting(repo, &browser_session.user).await?; + + let res = policy + .evaluate_compat_login(mas_policy::CompatLoginInput { + user: &browser_session.user, + login: CompatLogin::Token, + session_replaced, + session_counts, + requester, + }) + .await?; + if !res.valid() { + // If the only violation is that we have too many sessions, then handle that + // separately. + // In the future, we intend to evict some sessions automatically instead. 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. + if res.violations.len() == 1 { + let violation = &res.violations[0]; + if violation.code == Some(ViolationCode::TooManySessions) { + // The only violation is having reached the session limit. + return Err(RouteError::PolicyHardSessionLimitReached); + } + } + return Err(RouteError::PolicyRejected); + } + // We first create the session in the database, commit the transaction, then // create it on the homeserver, scheduling a device sync job afterwards to // make sure we don't end up in an inconsistent state. @@ -578,6 +638,8 @@ async fn user_password_login( limiter: &Limiter, requester: RequesterFingerprint, repo: &mut BoxRepository, + policy: &mut Policy, + policy_requester: Requester, username: &str, password: String, requested_device_id: Option, @@ -647,10 +709,38 @@ async fn user_password_login( Device::generate(&mut rng) }; - repo.app_session() + let session_replaced = repo + .app_session() .finish_sessions_to_replace_device(clock, &user, &device) .await?; + let session_counts = count_user_sessions_for_limiting(repo, &user).await?; + + let res = policy + .evaluate_compat_login(mas_policy::CompatLoginInput { + user: &user, + login: CompatLogin::Password, + session_replaced, + session_counts, + requester: policy_requester, + }) + .await?; + if !res.valid() { + // If the only violation is that we have too many sessions, then handle that + // separately. + // In the future, we intend to evict some sessions automatically instead. 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. + if res.violations.len() == 1 { + let violation = &res.violations[0]; + if violation.code == Some(ViolationCode::TooManySessions) { + // The only violation is having reached the session limit. + return Err(RouteError::PolicyHardSessionLimitReached); + } + } + return Err(RouteError::PolicyRejected); + } + let session = repo .compat_session() .add( diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index a4fbb24fb..c6d82d1ac 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -4,30 +4,35 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use anyhow::Context; use axum::{ extract::{Form, Path, State}, response::{Html, IntoResponse, Redirect, Response}, }; -use axum_extra::extract::Query; +use axum_extra::{TypedHeader, extract::Query}; use chrono::Duration; +use hyper::StatusCode; use mas_axum_utils::{ InternalError, cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{BoxClock, BoxRng, Clock}; +use mas_data_model::{BoxClock, BoxRng, Clock, MatrixUser}; +use mas_matrix::HomeserverConnection; +use mas_policy::{Policy, model::CompatLogin}; use mas_router::{CompatLoginSsoAction, UrlBuilder}; use mas_storage::{BoxRepository, RepositoryAccess, compat::CompatSsoLoginRepository}; -use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates}; +use mas_templates::{ + CompatLoginPolicyViolationContext, CompatSsoContext, ErrorContext, TemplateContext, Templates, +}; use serde::{Deserialize, Serialize}; use ulid::Ulid; use crate::{ - PreferredLanguage, - session::{SessionOrFallback, load_session_or_fallback}, + BoundActivityTracker, PreferredLanguage, + session::{SessionOrFallback, count_user_sessions_for_limiting, load_session_or_fallback}, }; #[derive(Serialize)] @@ -56,10 +61,16 @@ pub async fn get( mut repo: BoxRepository, State(templates): State, State(url_builder): State, + State(homeserver): State>, + mut policy: Policy, + activity_tracker: BoundActivityTracker, + user_agent: Option>, cookie_jar: CookieJar, Path(id): Path, Query(params): Query, ) -> Result { + let user_agent = user_agent.map(|ua| ua.to_string()); + let (cookie_jar, maybe_session) = match load_session_or_fallback( cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo, ) @@ -107,7 +118,69 @@ pub async fn get( return Ok((cookie_jar, Html(content)).into_response()); } - let ctx = CompatSsoContext::new(login) + let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?; + + // We can close the repository early, we don't need it at this point + repo.save().await?; + + let res = policy + .evaluate_compat_login(mas_policy::CompatLoginInput { + user: &session.user, + login: CompatLogin::Sso { + redirect_uri: login.redirect_uri.to_string(), + }, + // We don't know if there's going to be a replacement until we received the device ID, + // which happens too late. + session_replaced: false, + session_counts, + requester: mas_policy::Requester { + ip_address: activity_tracker.ip(), + user_agent, + }, + }) + .await?; + if !res.valid() { + let ctx = CompatLoginPolicyViolationContext::for_violations(res.violations) + .with_session(session) + .with_csrf(csrf_token.form_value()) + .with_language(locale); + + let content = templates.render_compat_login_policy_violation(&ctx)?; + + return Ok((StatusCode::FORBIDDEN, cookie_jar, Html(content)).into_response()); + } + + // Fetch informations about the user. This is purely cosmetic, so we let it + // fail and put a 1s timeout to it in case we fail to query it + // XXX: we're likely to need this in other places + let localpart = &session.user.username; + let display_name = match tokio::time::timeout( + std::time::Duration::from_secs(1), + homeserver.query_user(localpart), + ) + .await + { + Ok(Ok(user)) => user.displayname, + Ok(Err(err)) => { + tracing::warn!( + error = &*err as &dyn std::error::Error, + localpart, + "Failed to query user" + ); + None + } + Err(_) => { + tracing::warn!(localpart, "Timed out while querying user"); + None + } + }; + + let matrix_user = MatrixUser { + mxid: homeserver.mxid(localpart), + display_name, + }; + + let ctx = CompatSsoContext::new(login, matrix_user) .with_session(session) .with_csrf(csrf_token.form_value()) .with_language(locale); @@ -129,11 +202,16 @@ pub async fn post( PreferredLanguage(locale): PreferredLanguage, State(templates): State, State(url_builder): State, + mut policy: Policy, + activity_tracker: BoundActivityTracker, + user_agent: Option>, cookie_jar: CookieJar, Path(id): Path, Query(params): Query, Form(form): Form>, ) -> Result { + let user_agent = user_agent.map(|ua| ua.to_string()); + let (cookie_jar, maybe_session) = match load_session_or_fallback( cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo, ) @@ -200,6 +278,37 @@ pub async fn post( redirect_uri }; + let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?; + + let res = policy + .evaluate_compat_login(mas_policy::CompatLoginInput { + user: &session.user, + login: CompatLogin::Sso { + redirect_uri: login.redirect_uri.to_string(), + }, + session_counts, + // We don't know if there's going to be a replacement until we received the device ID, + // which happens too late. + session_replaced: false, + requester: mas_policy::Requester { + ip_address: activity_tracker.ip(), + user_agent, + }, + }) + .await?; + + if !res.valid() { + let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); + let ctx = CompatLoginPolicyViolationContext::for_violations(res.violations) + .with_session(session) + .with_csrf(csrf_token.form_value()) + .with_language(locale); + + let content = templates.render_compat_login_policy_violation(&ctx)?; + + return Ok((StatusCode::FORBIDDEN, cookie_jar, Html(content)).into_response()); + } + // Note that if the login is not Pending, // this fails and aborts the transaction. repo.compat_sso_login() diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 65a75f550..ebd223e4a 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -272,6 +272,7 @@ where BoxRepository: FromRequestParts, BoxClock: FromRequestParts, BoxRng: FromRequestParts, + Policy: FromRequestParts, { // A sub-router for human-facing routes with error handling let human_router = Router::new() diff --git a/crates/handlers/src/oauth2/authorization/consent.rs b/crates/handlers/src/oauth2/authorization/consent.rs index 2587828b5..ab51bef1c 100644 --- a/crates/handlers/src/oauth2/authorization/consent.rs +++ b/crates/handlers/src/oauth2/authorization/consent.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +use std::{sync::Arc, time::Duration}; + use axum::{ extract::{Form, Path, State}, response::{Html, IntoResponse, Response}, @@ -15,8 +17,9 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{AuthorizationGrantStage, BoxClock, BoxRng}; +use mas_data_model::{AuthorizationGrantStage, BoxClock, BoxRng, MatrixUser}; use mas_keystore::Keystore; +use mas_matrix::HomeserverConnection; use mas_policy::Policy; use mas_router::{PostAuthAction, UrlBuilder}; use mas_storage::{ @@ -87,6 +90,7 @@ pub(crate) async fn get( PreferredLanguage(locale): PreferredLanguage, State(templates): State, State(url_builder): State, + State(homeserver): State>, mut policy: Policy, mut repo: BoxRepository, activity_tracker: BoundActivityTracker, @@ -138,6 +142,9 @@ pub(crate) async fn get( let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?; + // We can close the repository early, we don't need it at this point + repo.save().await?; + let res = policy .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { user: Some(&session.user), @@ -162,7 +169,37 @@ pub(crate) async fn get( return Ok((cookie_jar, Html(content)).into_response()); } - let ctx = ConsentContext::new(grant, client) + // Fetch informations about the user. This is purely cosmetic, so we let it + // fail and put a 1s timeout to it in case we fail to query it + // XXX: we're likely to need this in other places + let localpart = &session.user.username; + let display_name = match tokio::time::timeout( + Duration::from_secs(1), + homeserver.query_user(localpart), + ) + .await + { + Ok(Ok(user)) => user.displayname, + Ok(Err(err)) => { + tracing::warn!( + error = &*err as &dyn std::error::Error, + localpart, + "Failed to query user" + ); + None + } + Err(_) => { + tracing::warn!(localpart, "Timed out while querying user"); + None + } + }; + + let matrix_user = MatrixUser { + mxid: homeserver.mxid(localpart), + display_name, + }; + + let ctx = ConsentContext::new(grant, client, matrix_user) .with_session(session) .with_csrf(csrf_token.form_value()) .with_language(locale); diff --git a/crates/handlers/src/oauth2/device/consent.rs b/crates/handlers/src/oauth2/device/consent.rs index e1d32870f..3912d2dc1 100644 --- a/crates/handlers/src/oauth2/device/consent.rs +++ b/crates/handlers/src/oauth2/device/consent.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +use std::{sync::Arc, time::Duration}; + use anyhow::Context; use axum::{ Form, @@ -16,7 +18,8 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{BoxClock, BoxRng}; +use mas_data_model::{BoxClock, BoxRng, MatrixUser}; +use mas_matrix::HomeserverConnection; use mas_policy::Policy; use mas_router::UrlBuilder; use mas_storage::BoxRepository; @@ -49,6 +52,7 @@ pub(crate) async fn get( PreferredLanguage(locale): PreferredLanguage, State(templates): State, State(url_builder): State, + State(homeserver): State>, mut repo: BoxRepository, mut policy: Policy, activity_tracker: BoundActivityTracker, @@ -105,6 +109,9 @@ pub(crate) async fn get( let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?; + // We can close the repository early, we don't need it at this point + repo.save().await?; + // Evaluate the policy let res = policy .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { @@ -133,7 +140,37 @@ pub(crate) async fn get( return Ok((cookie_jar, Html(content)).into_response()); } - let ctx = DeviceConsentContext::new(grant, client) + // Fetch informations about the user. This is purely cosmetic, so we let it + // fail and put a 1s timeout to it in case we fail to query it + // XXX: we're likely to need this in other places + let localpart = &session.user.username; + let display_name = match tokio::time::timeout( + Duration::from_secs(1), + homeserver.query_user(localpart), + ) + .await + { + Ok(Ok(user)) => user.displayname, + Ok(Err(err)) => { + tracing::warn!( + error = &*err as &dyn std::error::Error, + localpart, + "Failed to query user" + ); + None + } + Err(_) => { + tracing::warn!(localpart, "Timed out while querying user"); + None + } + }; + + let matrix_user = MatrixUser { + mxid: homeserver.mxid(localpart), + display_name, + }; + + let ctx = DeviceConsentContext::new(grant, client, matrix_user) .with_session(session) .with_csrf(csrf_token.form_value()) .with_language(locale); @@ -153,6 +190,7 @@ pub(crate) async fn post( PreferredLanguage(locale): PreferredLanguage, State(templates): State, State(url_builder): State, + State(homeserver): State>, mut repo: BoxRepository, mut policy: Policy, activity_tracker: BoundActivityTracker, @@ -265,7 +303,37 @@ pub(crate) async fn post( repo.save().await?; - let ctx = DeviceConsentContext::new(grant, client) + // Fetch informations about the user. This is purely cosmetic, so we let it + // fail and put a 1s timeout to it in case we fail to query it + // XXX: we're likely to need this in other places + let localpart = &session.user.username; + let display_name = match tokio::time::timeout( + Duration::from_secs(1), + homeserver.query_user(localpart), + ) + .await + { + Ok(Ok(user)) => user.displayname, + Ok(Err(err)) => { + tracing::warn!( + error = &*err as &dyn std::error::Error, + localpart, + "Failed to query user" + ); + None + } + Err(_) => { + tracing::warn!(localpart, "Timed out while querying user"); + None + } + }; + + let matrix_user = MatrixUser { + mxid: homeserver.mxid(localpart), + display_name, + }; + + let ctx = DeviceConsentContext::new(grant, client, matrix_user) .with_session(session) .with_csrf(csrf_token.form_value()) .with_language(locale); diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index 4b93177de..521a4848d 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -82,6 +82,7 @@ pub(crate) async fn policy_factory( register: "register/violation".to_owned(), client_registration: "client_registration/violation".to_owned(), authorization_grant: "authorization_grant/violation".to_owned(), + compat_login: "compat_login/violation".to_owned(), email: "email/violation".to_owned(), }; diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index c6c1de95d..ba24ed311 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -4,7 +4,10 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. -use std::sync::{Arc, LazyLock}; +use std::{ + net::IpAddr, + sync::{Arc, LazyLock}, +}; use axum::{ Form, @@ -19,14 +22,19 @@ use mas_axum_utils::{ csrf::{CsrfExt, ProtectedForm}, record_error, }; -use mas_data_model::{BoxClock, BoxRng, UpstreamOAuthProviderOnConflict}; +use mas_data_model::{ + BoxClock, BoxRng, UpstreamOAuthAuthorizationSession, UpstreamOAuthProviderOnConflict, + UserRegistration, +}; use mas_jose::jwt::Jwt; use mas_matrix::HomeserverConnection; use mas_policy::Policy; use mas_router::UrlBuilder; use mas_storage::{ - BoxRepository, RepositoryAccess, - upstream_oauth2::{UpstreamOAuthLinkRepository, UpstreamOAuthSessionRepository}, + BoxRepository, Pagination, RepositoryAccess, + upstream_oauth2::{ + UpstreamOAuthLinkFilter, UpstreamOAuthLinkRepository, UpstreamOAuthSessionRepository, + }, user::{BrowserSessionRepository, UserEmailRepository, UserRepository}, }; use mas_templates::{ @@ -238,10 +246,6 @@ pub(crate) async fn get( .lookup_link(link_id) .map_err(|_| RouteError::MissingCookie)?; - let post_auth_action = OptionalPostAuthAction { - post_auth_action: post_auth_action.cloned(), - }; - let link = repo .upstream_oauth_link() .lookup(link_id) @@ -285,6 +289,10 @@ pub(crate) async fn get( repo.save().await?; + let post_auth_action = OptionalPostAuthAction { + post_auth_action: post_auth_action.cloned(), + }; + post_auth_action.go_next(&url_builder).into_response() } @@ -357,6 +365,10 @@ pub(crate) async fn get( .authenticate_with_upstream(&mut rng, &clock, &session, &upstream_session) .await?; + let post_auth_action = OptionalPostAuthAction { + post_auth_action: post_auth_action.cloned(), + }; + cookie_jar = sessions_cookie .consume_link(link_id)? .save(cookie_jar, &clock); @@ -386,8 +398,6 @@ pub(crate) async fn get( .await? .ok_or(RouteError::ProviderNotFound(link.provider_id))?; - let mut ctx = UpstreamRegister::new(link.clone(), provider.clone()); - let env = environment(); let mut context = AttributeMappingContext::new(); @@ -421,13 +431,6 @@ pub(crate) async fn get( )? }; - if let Some(displayname) = displayname { - ctx = ctx.with_display_name( - displayname, - provider.claims_imports.displayname.is_forced_or_required(), - ); - } - let email = if provider.claims_imports.email.ignore() { None } else { @@ -446,13 +449,6 @@ pub(crate) async fn get( )? }; - if let Some(ref email) = email { - ctx = ctx.with_email( - email.clone(), - provider.claims_imports.email.is_forced_or_required(), - ); - } - // We do a bunch of checks for the localpart. Instead of using nested ifs all // the way, we use a labelled block, and use `break` for 'exiting' early when // needed @@ -585,6 +581,92 @@ pub(crate) async fn get( .associate_to_user(&link, &existing_user) .await?; } + + // We matched an existing user and the conflict resolution is to replace any + // link on the existing user with this one + UpstreamOAuthProviderOnConflict::Replace => { + // Find existing links for this provider and user + let filter = UpstreamOAuthLinkFilter::new() + .for_provider(&provider) + .for_user(&existing_user); + let mut cursor = Pagination::first(100); + let mut removed = 0; + loop { + let page = repo.upstream_oauth_link().list(filter, cursor).await?; + for edge in page.edges { + // Remove any existing links for this provider and user + repo.upstream_oauth_link().remove(&clock, edge.node).await?; + cursor = cursor.after(edge.cursor); + removed += 1; + } + + if !page.has_next_page { + break; + } + } + + if removed > 0 { + tracing::warn!( + user.id = %existing_user.id, + upstream_oauth_provider.id = %provider.id, + upstream_oauth_link.id = %link.id, + upstream_oauth_link.subject = link.subject, + "Upstream account mapped localpart {localpart:?} matched an existing user, replaced {removed} links" + ); + } else { + tracing::info!( + user.id = %existing_user.id, + upstream_oauth_provider.id = %provider.id, + upstream_oauth_link.id = %link.id, + upstream_oauth_link.subject = link.subject, + "Upstream account mapped localpart {localpart:?} matched an existing user, linking" + ); + } + + // Add link to the user + repo.upstream_oauth_link() + .associate_to_user(&link, &existing_user) + .await?; + } + + // We matched an existing user and the conflict resolution is to link to the + // existing user *only if* there is no existing link on that user + UpstreamOAuthProviderOnConflict::Set => { + // Find existing links for this provider and user + let filter = UpstreamOAuthLinkFilter::new() + .for_provider(&provider) + .for_user(&existing_user); + + let count = repo.upstream_oauth_link().count(filter).await?; + if count > 0 { + tracing::warn!( + upstream_oauth_provider.id = %provider.id, + upstream_oauth_link.id = %link.id, + user.id = %existing_user.id, + "Upstream provider returned a localpart {localpart:?} matching an existing user who already has {count} link(s) to this provider, which isn't allowed by the conflict resolution" + ); + + // TODO: translate + let ctx = ErrorContext::new() + .with_code("User exists") + .with_description(format!( + r"Upstream account provider returned {localpart:?} as username, + but this user already has an existing link to this provider. + Your homeserver does not allow replacing upstream account links automatically." + )) + .with_language(&locale); + + return Ok(( + cookie_jar, + Html(templates.render_error(&ctx)?).into_response(), + )); + } + + // Add link to the user + repo.upstream_oauth_link() + .associate_to_user(&link, &existing_user) + .await?; + } } // Now that we've resolved the conflict, log in that existing user @@ -622,6 +704,10 @@ pub(crate) async fn get( .authenticate_with_upstream(&mut rng, &clock, &session, &upstream_session) .await?; + let post_auth_action = OptionalPostAuthAction { + post_auth_action: post_auth_action.cloned(), + }; + let cookie_jar = sessions_cookie .consume_link(link_id)? .save(cookie_jar, &clock) @@ -679,6 +765,53 @@ pub(crate) async fn get( Some(localpart) }; + if provider.claims_imports.skip_confirmation { + let Some(localpart) = localpart else { + return Err(RouteError::Internal( + "No localpart available even though the provider is configured to skip confirmation, this is a bug!".into() + )); + }; + + // Register on the fly + REGISTRATION_COUNTER.add(1, &[KeyValue::new(PROVIDER, provider.id.to_string())]); + + let registration = prepare_user_registration( + &mut rng, + &clock, + &mut repo, + upstream_session, + localpart, + displayname, + email, + activity_tracker.ip(), + user_agent, + post_auth_action.map(|action| serde_json::json!(action)), + ) + .await?; + + let registrations = UserRegistrationSessionsCookie::load(&cookie_jar); + + let cookie_jar = sessions_cookie + .consume_link(link_id)? + .save(cookie_jar, &clock); + + let cookie_jar = registrations.add(®istration).save(cookie_jar, &clock); + + repo.save().await?; + + // Redirect to the user registration flow, in case we have any other step to + // finish + return Ok(( + cookie_jar, + url_builder + .redirect(&mas_router::RegisterFinish::new(registration.id)) + .into_response(), + )); + } + + // Else we show the upstream registration screen + let mut ctx = UpstreamRegister::new(link.clone(), provider.clone()); + if let Some(localpart) = localpart { ctx = ctx.with_localpart( localpart, @@ -686,6 +819,17 @@ pub(crate) async fn get( ); } + if let Some(displayname) = displayname { + ctx = ctx.with_display_name( + displayname, + provider.claims_imports.displayname.is_forced_or_required(), + ); + } + + if let Some(email) = email { + ctx = ctx.with_email(email, provider.claims_imports.email.is_forced_or_required()); + } + let ctx = ctx.with_csrf(csrf_token.form_value()).with_language(locale); Html(templates.render_upstream_oauth2_do_register(&ctx)?).into_response() @@ -1002,17 +1146,19 @@ pub(crate) async fn post( REGISTRATION_COUNTER.add(1, &[KeyValue::new(PROVIDER, provider.id.to_string())]); - let mut registration = repo - .user_registration() - .add( - &mut rng, - &clock, - username, - activity_tracker.ip(), - user_agent, - post_auth_action.map(|action| serde_json::json!(action)), - ) - .await?; + let mut registration = prepare_user_registration( + &mut rng, + &clock, + &mut repo, + upstream_session, + username, + display_name, + email, + activity_tracker.ip(), + user_agent, + post_auth_action.map(|action| serde_json::json!(action)), + ) + .await?; if let Some(terms_url) = &site_config.tos_uri { registration = repo @@ -1021,44 +1167,6 @@ pub(crate) async fn post( .await?; } - // If we have an email, add an email authentication and complete it - if let Some(email) = email { - let authentication = repo - .user_email() - .add_authentication_for_registration(&mut rng, &clock, email, ®istration) - .await?; - let authentication = repo - .user_email() - .complete_authentication_with_upstream( - &clock, - authentication, - &upstream_session, - ) - .await?; - - registration = repo - .user_registration() - .set_email_authentication(registration, &authentication) - .await?; - } - - // If we have a display name, add it to the registration - if let Some(name) = display_name { - registration = repo - .user_registration() - .set_display_name(registration, name) - .await?; - } - - let registration = repo - .user_registration() - .set_upstream_oauth_authorization_session(registration, &upstream_session) - .await?; - - repo.upstream_oauth_session() - .consume(&clock, upstream_session) - .await?; - let registrations = UserRegistrationSessionsCookie::load(&cookie_jar); let cookie_jar = sessions_cookie @@ -1082,6 +1190,69 @@ pub(crate) async fn post( } } +/// Create a user registration using attributes got from the upstream +/// authorization session +async fn prepare_user_registration( + rng: &mut BoxRng, + clock: &BoxClock, + repo: &mut BoxRepository, + upstream_session: UpstreamOAuthAuthorizationSession, + localpart: String, + displayname: Option, + email: Option, + ip_address: Option, + user_agent: Option, + post_auth_action: Option, +) -> Result { + let mut registration = repo + .user_registration() + .add( + rng, + clock, + localpart, + ip_address, + user_agent, + post_auth_action, + ) + .await?; + + // If we have an email, add an email authentication and complete it + if let Some(email) = email { + let authentication = repo + .user_email() + .add_authentication_for_registration(rng, clock, email, ®istration) + .await?; + let authentication = repo + .user_email() + .complete_authentication_with_upstream(clock, authentication, &upstream_session) + .await?; + + registration = repo + .user_registration() + .set_email_authentication(registration, &authentication) + .await?; + } + + // If we have a display name, add it to the registration + if let Some(name) = displayname { + registration = repo + .user_registration() + .set_display_name(registration, name) + .await?; + } + + let registration = repo + .user_registration() + .set_upstream_oauth_authorization_session(registration, &upstream_session) + .await?; + + repo.upstream_oauth_session() + .consume(clock, upstream_session) + .await?; + + Ok(registration) +} + #[cfg(test)] mod tests { use hyper::{Request, StatusCode, header::CONTENT_TYPE}; @@ -1298,6 +1469,178 @@ mod tests { assert!(email_auth.completed_at.is_some()); } + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_skip_confirmation(pool: PgPool) { + // Same test as test_register, but checks that we get straight to the + // registration flow skipping the confirmation + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + let mut rng = state.rng(); + let cookies = CookieHelper::new(); + + let claims_imports = UpstreamOAuthProviderClaimsImports { + skip_confirmation: true, + localpart: UpstreamOAuthProviderLocalpartPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::default(), + }, + email: UpstreamOAuthProviderImportPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Force, + template: None, + }, + ..UpstreamOAuthProviderClaimsImports::default() + }; + + let id_token_claims = serde_json::json!({ + "preferred_username": "john", + "email": "john@example.com", + "email_verified": true, + }); + + // Grab a key to sign the id_token + // We could generate a key on the fly, but because we have one available here, + // why not use it? + let key = state + .key_store + .signing_key_for_algorithm(&JsonWebSignatureAlg::Rs256) + .unwrap(); + + let signer = key + .params() + .signing_key_for_alg(&JsonWebSignatureAlg::Rs256) + .unwrap(); + let header = JsonWebSignatureHeader::new(JsonWebSignatureAlg::Rs256); + let id_token = + Jwt::sign_with_rng(&mut rng, header, id_token_claims.clone(), &signer).unwrap(); + + // Provision a provider and a link + let mut repo = state.repository().await.unwrap(); + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &state.clock, + UpstreamOAuthProviderParams { + issuer: Some("https://example.com/".to_owned()), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, + scope: Scope::from_iter([OPENID]), + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None, + token_endpoint_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + client_id: "client".to_owned(), + encrypted_client_secret: None, + claims_imports, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + ui_order: 0, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + }, + ) + .await + .unwrap(); + + let session = repo + .upstream_oauth_session() + .add( + &mut rng, + &state.clock, + &provider, + "state".to_owned(), + None, + None, + ) + .await + .unwrap(); + + let link = repo + .upstream_oauth_link() + .add( + &mut rng, + &state.clock, + &provider, + "subject".to_owned(), + None, + ) + .await + .unwrap(); + + let session = repo + .upstream_oauth_session() + .complete_with_link( + &state.clock, + session, + &link, + Some(id_token.into_string()), + Some(id_token_claims), + None, + None, + ) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let upstream_sessions = UpstreamSessionsCookie::default() + .add(session.id, provider.id, "state".to_owned(), None) + .add_link_to_session(session.id, link.id) + .unwrap(); + let cookie_jar = upstream_sessions.save(cookie_jar, &state.clock); + cookies.import(cookie_jar); + + let request = Request::get(&*mas_router::UpstreamOAuth2Link::new(link.id).path()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + let location = response.headers().get(hyper::header::LOCATION).unwrap(); + // Grab the registration ID from the redirected URL: + // /register/steps/{id}/finish + let registration_id: Ulid = str::from_utf8(location.as_bytes()) + .unwrap() + .rsplit('/') + .nth(1) + .expect("Location to have two slashes") + .parse() + .expect("last segment of location to be a ULID"); + + // Check that we have a registered user, with the email imported + let mut repo = state.repository().await.unwrap(); + let registration: UserRegistration = repo + .user_registration() + .lookup(registration_id) + .await + .unwrap() + .expect("user registration exists"); + + assert_eq!(registration.password, None); + assert_eq!(registration.completed_at, None); + assert_eq!(registration.username, "john"); + + let email_auth_id = registration + .email_authentication_id + .expect("registration should have an email authentication"); + let email_auth: UserEmailAuthentication = repo + .user_email() + .lookup_authentication(email_auth_id) + .await + .unwrap() + .expect("email authentication should exist"); + assert_eq!(email_auth.email, "john@example.com"); + assert!(email_auth.completed_at.is_some()); + } + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] async fn test_link_existing_account(pool: PgPool) { let existing_username = "john"; @@ -1583,4 +1926,427 @@ mod tests { Ok((link, session)) } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_link_existing_account_replace_conflict(pool: PgPool) { + let existing_username = "john"; + let subject = "subject"; + let old_subject = "old_subject"; + + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + let mut rng = state.rng(); + let cookies = CookieHelper::new(); + + let claims_imports = UpstreamOAuthProviderClaimsImports { + localpart: UpstreamOAuthProviderLocalpartPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + // This will replace any existing links for this provider and user + on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::Replace, + }, + email: UpstreamOAuthProviderImportPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + }, + ..UpstreamOAuthProviderClaimsImports::default() + }; + + let id_token_claims = serde_json::json!({ + "preferred_username": existing_username, + "email": "any@example.com", + "email_verified": true, + }); + + let id_token = sign_token(&mut rng, &state.key_store, id_token_claims.clone()).unwrap(); + + // Provision a provider and a link + let mut repo = state.repository().await.unwrap(); + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &state.clock, + UpstreamOAuthProviderParams { + issuer: Some("https://example.com/".to_owned()), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, + scope: Scope::from_iter([OPENID]), + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None, + token_endpoint_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + client_id: "client".to_owned(), + encrypted_client_secret: None, + claims_imports, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }, + ) + .await + .unwrap(); + + // Create an existing user + let user = repo + .user() + .add(&mut rng, &state.clock, existing_username.to_owned()) + .await + .unwrap(); + + // Create an existing link for this user and provider with a different subject + let old_link = repo + .upstream_oauth_link() + .add( + &mut rng, + &state.clock, + &provider, + old_subject.to_owned(), + None, + ) + .await + .unwrap(); + + repo.upstream_oauth_link() + .associate_to_user(&old_link, &user) + .await + .unwrap(); + + // Provision upstream authorization session to setup cookies + let (link, session) = add_linked_upstream_session( + &mut rng, + &state.clock, + &mut repo, + &provider, + subject, + &id_token.into_string(), + id_token_claims, + ) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let upstream_sessions = UpstreamSessionsCookie::default() + .add(session.id, provider.id, "state".to_owned(), None) + .add_link_to_session(session.id, link.id) + .unwrap(); + let cookie_jar = upstream_sessions.save(cookie_jar, &state.clock); + cookies.import(cookie_jar); + + let request = Request::get(&*mas_router::UpstreamOAuth2Link::new(link.id).path()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::SEE_OTHER); + + // Check that the new link is associated with the existing user + let mut repo = state.repository().await.unwrap(); + + let new_link = repo + .upstream_oauth_link() + .find_by_subject(&provider, subject) + .await + .unwrap() + .expect("new link exists"); + + assert_eq!(new_link.user_id, Some(user.id)); + + // Check that the old link was removed + let old_link_result = repo + .upstream_oauth_link() + .find_by_subject(&provider, old_subject) + .await + .unwrap(); + + assert!( + old_link_result.is_none(), + "Old link should have been removed" + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_link_existing_account_set_conflict_success(pool: PgPool) { + let existing_username = "john"; + let subject = "subject"; + + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + let mut rng = state.rng(); + let cookies = CookieHelper::new(); + + let claims_imports = UpstreamOAuthProviderClaimsImports { + localpart: UpstreamOAuthProviderLocalpartPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + // This will only link if there are no existing links for this provider and user + on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::Set, + }, + email: UpstreamOAuthProviderImportPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + }, + ..UpstreamOAuthProviderClaimsImports::default() + }; + + let id_token_claims = serde_json::json!({ + "preferred_username": existing_username, + "email": "any@example.com", + "email_verified": true, + }); + + let id_token = sign_token(&mut rng, &state.key_store, id_token_claims.clone()).unwrap(); + + // Provision a provider and a link + let mut repo = state.repository().await.unwrap(); + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &state.clock, + UpstreamOAuthProviderParams { + issuer: Some("https://example.com/".to_owned()), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, + scope: Scope::from_iter([OPENID]), + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None, + token_endpoint_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + client_id: "client".to_owned(), + encrypted_client_secret: None, + claims_imports, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }, + ) + .await + .unwrap(); + + // Create an existing user (with no existing links for this provider) + let user = repo + .user() + .add(&mut rng, &state.clock, existing_username.to_owned()) + .await + .unwrap(); + + // Provision upstream authorization session to setup cookies + let (link, session) = add_linked_upstream_session( + &mut rng, + &state.clock, + &mut repo, + &provider, + subject, + &id_token.into_string(), + id_token_claims, + ) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let upstream_sessions = UpstreamSessionsCookie::default() + .add(session.id, provider.id, "state".to_owned(), None) + .add_link_to_session(session.id, link.id) + .unwrap(); + let cookie_jar = upstream_sessions.save(cookie_jar, &state.clock); + cookies.import(cookie_jar); + + let request = Request::get(&*mas_router::UpstreamOAuth2Link::new(link.id).path()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::SEE_OTHER); + + // Check that the new link is associated with the existing user + let mut repo = state.repository().await.unwrap(); + + let new_link = repo + .upstream_oauth_link() + .find_by_subject(&provider, subject) + .await + .unwrap() + .expect("new link exists"); + + assert_eq!(new_link.user_id, Some(user.id)); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_link_existing_account_set_conflict_failure(pool: PgPool) { + let existing_username = "john"; + let subject = "subject"; + let old_subject = "old_subject"; + + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + let mut rng = state.rng(); + let cookies = CookieHelper::new(); + + let claims_imports = UpstreamOAuthProviderClaimsImports { + localpart: UpstreamOAuthProviderLocalpartPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + // This will only link if there are no existing links for this provider and user + on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::Set, + }, + email: UpstreamOAuthProviderImportPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + }, + ..UpstreamOAuthProviderClaimsImports::default() + }; + + let id_token_claims = serde_json::json!({ + "preferred_username": existing_username, + "email": "any@example.com", + "email_verified": true, + }); + + let id_token = sign_token(&mut rng, &state.key_store, id_token_claims.clone()).unwrap(); + + // Provision a provider and a link + let mut repo = state.repository().await.unwrap(); + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &state.clock, + UpstreamOAuthProviderParams { + issuer: Some("https://example.com/".to_owned()), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, + scope: Scope::from_iter([OPENID]), + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None, + token_endpoint_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + client_id: "client".to_owned(), + encrypted_client_secret: None, + claims_imports, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }, + ) + .await + .unwrap(); + + // Create an existing user + let user = repo + .user() + .add(&mut rng, &state.clock, existing_username.to_owned()) + .await + .unwrap(); + + // Create an existing link for this user and provider with a different subject + let old_link = repo + .upstream_oauth_link() + .add( + &mut rng, + &state.clock, + &provider, + old_subject.to_owned(), + None, + ) + .await + .unwrap(); + + repo.upstream_oauth_link() + .associate_to_user(&old_link, &user) + .await + .unwrap(); + + // Provision upstream authorization session to setup cookies + let (link, session) = add_linked_upstream_session( + &mut rng, + &state.clock, + &mut repo, + &provider, + subject, + &id_token.into_string(), + id_token_claims, + ) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let upstream_sessions = UpstreamSessionsCookie::default() + .add(session.id, provider.id, "state".to_owned(), None) + .add_link_to_session(session.id, link.id) + .unwrap(); + let cookie_jar = upstream_sessions.save(cookie_jar, &state.clock); + cookies.import(cookie_jar); + + let request = Request::get(&*mas_router::UpstreamOAuth2Link::new(link.id).path()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + + // Should return an error page because the user already has a link for this + // provider + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Verify the error message is displayed + assert!(response.body().contains("User exists")); + assert!(response.body().contains("replacing upstream account links")); + + // Check that the new link was NOT associated with the existing user + let mut repo = state.repository().await.unwrap(); + + let new_link = repo + .upstream_oauth_link() + .find_by_subject(&provider, subject) + .await + .unwrap() + .expect("new link exists"); + + // The new link should still not be associated with the user + assert_eq!(new_link.user_id, None); + + // Check that the old link is still there + let old_link_result = repo + .upstream_oauth_link() + .find_by_subject(&provider, old_subject) + .await + .unwrap(); + + assert!(old_link_result.is_some(), "Old link should still exist"); + assert_eq!(old_link_result.unwrap().user_id, Some(user.id)); + } } diff --git a/crates/policy/src/bin/schema.rs b/crates/policy/src/bin/schema.rs index 8e9c81a07..be778f6e1 100644 --- a/crates/policy/src/bin/schema.rs +++ b/crates/policy/src/bin/schema.rs @@ -12,7 +12,7 @@ use std::path::{Path, PathBuf}; use mas_policy::model::{ - AuthorizationGrantInput, ClientRegistrationInput, EmailInput, RegisterInput, + AuthorizationGrantInput, ClientRegistrationInput, CompatLoginInput, EmailInput, RegisterInput, }; use schemars::{JsonSchema, generate::SchemaSettings}; @@ -42,5 +42,6 @@ fn main() { write_schema::(output_root, "register_input.json"); write_schema::(output_root, "client_registration_input.json"); write_schema::(output_root, "authorization_grant_input.json"); + write_schema::(output_root, "compat_login_input.json"); write_schema::(output_root, "email_input.json"); } diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index 8a038aea8..dcb68dd36 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -19,8 +19,9 @@ use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt}; pub use self::model::{ - AuthorizationGrantInput, ClientRegistrationInput, Code as ViolationCode, EmailInput, - EvaluationResult, GrantType, RegisterInput, RegistrationMethod, Requester, Violation, + AuthorizationGrantInput, ClientRegistrationInput, Code as ViolationCode, CompatLoginInput, + EmailInput, EvaluationResult, GrantType, RegisterInput, RegistrationMethod, Requester, + Violation, }; #[derive(Debug, Error)] @@ -72,15 +73,17 @@ pub struct Entrypoints { pub register: String, pub client_registration: String, pub authorization_grant: String, + pub compat_login: String, pub email: String, } impl Entrypoints { - fn all(&self) -> [&str; 4] { + fn all(&self) -> [&str; 5] { [ self.register.as_str(), self.client_registration.as_str(), self.authorization_grant.as_str(), + self.compat_login.as_str(), self.email.as_str(), ] } @@ -459,6 +462,30 @@ impl Policy { Ok(res) } + + /// Evaluate the `compat_login` entrypoint. + /// + /// # Errors + /// + /// Returns an error if the policy engine fails to evaluate the entrypoint. + #[tracing::instrument( + name = "policy.evaluate.compat_login", + skip_all, + fields( + %input.user.id, + ), + )] + pub async fn evaluate_compat_login( + &mut self, + input: CompatLoginInput<'_>, + ) -> Result { + let [res]: [EvaluationResult; 1] = self + .instance + .evaluate(&mut self.store, &self.entrypoints.compat_login, &input) + .await?; + + Ok(res) + } } #[cfg(test)] @@ -468,6 +495,16 @@ mod tests { use super::*; + fn make_entrypoints() -> Entrypoints { + Entrypoints { + register: "register/violation".to_owned(), + client_registration: "client_registration/violation".to_owned(), + authorization_grant: "authorization_grant/violation".to_owned(), + compat_login: "compat_login/violation".to_owned(), + email: "email/violation".to_owned(), + } + } + #[tokio::test] async fn test_register() { let data = Data::new("example.com".to_owned(), None).with_rest(serde_json::json!({ @@ -484,14 +521,9 @@ mod tests { let file = tokio::fs::File::open(path).await.unwrap(); - let entrypoints = Entrypoints { - register: "register/violation".to_owned(), - client_registration: "client_registration/violation".to_owned(), - authorization_grant: "authorization_grant/violation".to_owned(), - email: "email/violation".to_owned(), - }; - - let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap(); + let factory = PolicyFactory::load(file, data, make_entrypoints()) + .await + .unwrap(); let mut policy = factory.instantiate().await.unwrap(); @@ -551,14 +583,9 @@ mod tests { let file = tokio::fs::File::open(path).await.unwrap(); - let entrypoints = Entrypoints { - register: "register/violation".to_owned(), - client_registration: "client_registration/violation".to_owned(), - authorization_grant: "authorization_grant/violation".to_owned(), - email: "email/violation".to_owned(), - }; - - let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap(); + let factory = PolicyFactory::load(file, data, make_entrypoints()) + .await + .unwrap(); let mut policy = factory.instantiate().await.unwrap(); @@ -620,14 +647,9 @@ mod tests { let file = tokio::fs::File::open(path).await.unwrap(); - let entrypoints = Entrypoints { - register: "register/violation".to_owned(), - client_registration: "client_registration/violation".to_owned(), - authorization_grant: "authorization_grant/violation".to_owned(), - email: "email/violation".to_owned(), - }; - - let factory = PolicyFactory::load(file, data, entrypoints).await.unwrap(); + let factory = PolicyFactory::load(file, data, make_entrypoints()) + .await + .unwrap(); // That is around 1 MB of JSON data. Each element is a 5-digit string, so 8 // characters including the quotes and a comma. diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index b85170025..a9f5fb502 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -17,7 +17,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// A well-known policy code. -#[derive(Deserialize, Debug, Clone, Copy, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum Code { /// The username is too short. @@ -75,7 +75,7 @@ impl Code { } /// A single violation of a policy. -#[derive(Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct Violation { pub msg: String, pub redirect_uri: Option, @@ -187,6 +187,42 @@ pub struct AuthorizationGrantInput<'a> { pub requester: Requester, } +/// Input for the compatibility login policy. +#[derive(Serialize, Debug, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct CompatLoginInput<'a> { + #[schemars(with = "std::collections::HashMap")] + pub user: &'a User, + + /// How many sessions the user has. + pub session_counts: SessionCounts, + + /// Whether a session will be replaced by this login + pub session_replaced: bool, + + /// What type of login is being performed. + /// This also determines whether the login is interactive. + pub login: CompatLogin, + + pub requester: Requester, +} + +#[derive(Serialize, Debug, JsonSchema)] +#[serde(tag = "type")] +pub enum CompatLogin { + /// Used as the interactive part of SSO login. + #[serde(rename = "m.login.sso")] + Sso { redirect_uri: String }, + + /// Used as the final (non-interactive) stage of SSO login. + #[serde(rename = "m.login.token")] + Token, + + /// Non-interactive password-over-the-API login. + #[serde(rename = "m.login.password")] + Password, +} + /// Information about how many sessions the user has #[derive(Serialize, Debug, JsonSchema)] pub struct SessionCounts { diff --git a/crates/storage-pg/.sqlx/query-2f66991d7b9ba58f011d9aef0eb6a38f3b244c2f46444c0ab345de7feff54aba.json b/crates/storage-pg/.sqlx/query-2f66991d7b9ba58f011d9aef0eb6a38f3b244c2f46444c0ab345de7feff54aba.json new file mode 100644 index 000000000..7fb8be867 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-2f66991d7b9ba58f011d9aef0eb6a38f3b244c2f46444c0ab345de7feff54aba.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT current_database() as \"current_database!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "current_database!", + "type_info": "Name" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "2f66991d7b9ba58f011d9aef0eb6a38f3b244c2f46444c0ab345de7feff54aba" +} diff --git a/crates/storage-pg/.sqlx/query-fbf926f630df5d588df4f1c9c0dc0f594332be5829d5d7c6b66183ac25b3d166.json b/crates/storage-pg/.sqlx/query-fbf926f630df5d588df4f1c9c0dc0f594332be5829d5d7c6b66183ac25b3d166.json new file mode 100644 index 000000000..d41b1dccd --- /dev/null +++ b/crates/storage-pg/.sqlx/query-fbf926f630df5d588df4f1c9c0dc0f594332be5829d5d7c6b66183ac25b3d166.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT EXISTS (\n SELECT 1\n FROM information_schema.tables\n WHERE table_name = '_sqlx_migrations'\n ) AS \"exists!\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "fbf926f630df5d588df4f1c9c0dc0f594332be5829d5d7c6b66183ac25b3d166" +} diff --git a/crates/storage-pg/Cargo.toml b/crates/storage-pg/Cargo.toml index 8710ead70..c058c3c14 100644 --- a/crates/storage-pg/Cargo.toml +++ b/crates/storage-pg/Cargo.toml @@ -19,6 +19,7 @@ workspace = true [dependencies] async-trait.workspace = true chrono.workspace = true +crc.workspace = true futures-util.workspace = true opentelemetry-semantic-conventions.workspace = true opentelemetry.workspace = true @@ -31,6 +32,7 @@ sha2.workspace = true sqlx.workspace = true thiserror.workspace = true tracing.workspace = true +tokio.workspace = true ulid.workspace = true url.workspace = true uuid.workspace = true diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index 4e12810cc..2867534c3 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -487,14 +487,15 @@ impl AppSessionRepository for PgAppSessionRepository<'_> { clock: &dyn Clock, user: &User, device: &Device, - ) -> Result<(), Self::Error> { + ) -> Result { + let mut affected = false; // TODO need to invoke this from all the oauth2 login sites let span = tracing::info_span!( "db.app_session.finish_sessions_to_replace_device.compat_sessions", { DB_QUERY_TEXT } = tracing::field::Empty, ); let finished_at = clock.now(); - sqlx::query!( + let compat_affected = sqlx::query!( " UPDATE compat_sessions SET finished_at = $3 WHERE user_id = $1 AND device_id = $2 AND finished_at IS NULL ", @@ -505,7 +506,9 @@ impl AppSessionRepository for PgAppSessionRepository<'_> { .record(&span) .execute(&mut *self.conn) .instrument(span) - .await?; + .await? + .rows_affected(); + affected |= compat_affected > 0; if let Ok([stable_device_as_scope_token, unstable_device_as_scope_token]) = device.to_scope_token() @@ -514,7 +517,7 @@ impl AppSessionRepository for PgAppSessionRepository<'_> { "db.app_session.finish_sessions_to_replace_device.oauth2_sessions", { DB_QUERY_TEXT } = tracing::field::Empty, ); - sqlx::query!( + let oauth2_affected = sqlx::query!( " UPDATE oauth2_sessions SET finished_at = $4 @@ -530,10 +533,12 @@ impl AppSessionRepository for PgAppSessionRepository<'_> { .record(&span) .execute(&mut *self.conn) .instrument(span) - .await?; + .await? + .rows_affected(); + affected |= oauth2_affected > 0; } - Ok(()) + Ok(affected) } } diff --git a/crates/storage-pg/src/lib.rs b/crates/storage-pg/src/lib.rs index 207235667..ea2029475 100644 --- a/crates/storage-pg/src/lib.rs +++ b/crates/storage-pg/src/lib.rs @@ -160,7 +160,15 @@ #![deny(clippy::future_not_send, missing_docs)] #![allow(clippy::module_name_repetitions, clippy::blocks_in_conditions)] -use sqlx::migrate::Migrator; +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +use ::tracing::{Instrument, debug, info, info_span, warn}; +use opentelemetry_semantic_conventions::trace::DB_QUERY_TEXT; +use sqlx::{ + Either, PgConnection, + migrate::{AppliedMigration, Migrate, MigrateError, Migration, Migrator}, + postgres::{PgAdvisoryLock, PgAdvisoryLockKey}, +}; pub mod app_session; pub mod compat; @@ -186,14 +194,290 @@ pub use self::{ tracing::ExecuteExt, }; -/// Embedded migrations, allowing them to run on startup -pub static MIGRATOR: Migrator = { - // XXX: The macro does not let us ignore missing migrations, so we have to do it - // like this. See https://github.com/launchbadge/sqlx/issues/1788 - let mut m = sqlx::migrate!(); +/// Embedded migrations in the binary +pub static MIGRATOR: Migrator = sqlx::migrate!(); - // We manually removed some migrations because they made us depend on the - // `pgcrypto` extension. See: https://github.com/matrix-org/matrix-authentication-service/issues/1557 - m.ignore_missing = true; - m -}; +fn available_migrations() -> BTreeMap { + MIGRATOR.iter().map(|m| (m.version, m)).collect() +} + +/// This is the list of migrations we've removed from the migration history but +/// might have been applied in the past +#[allow(clippy::inconsistent_digit_grouping)] +const ALLOWED_MISSING_MIGRATIONS: &[i64] = &[ + // https://github.com/matrix-org/matrix-authentication-service/pull/1585 + 20220709_210445, + 20230330_210841, + 20230408_110421, +]; + +fn allowed_missing_migrations() -> BTreeSet { + ALLOWED_MISSING_MIGRATIONS.iter().copied().collect() +} + +/// This is a list of possible additional checksums from previous versions of +/// migrations. The checksum we store in the database is 48 bytes long. We're +/// not really concerned with partial hash collisions, and to avoid this file to +/// be completely unreadable, we only store the upper 16 bytes of that hash. +#[allow(clippy::inconsistent_digit_grouping)] +const ALLOWED_ALTERNATE_CHECKSUMS: &[(i64, u128)] = &[ + // https://github.com/element-hq/matrix-authentication-service/pull/5300 + (20250410_000000, 0x8811_c3ef_dbee_8c00_5b49_25da_5d55_9c3f), + (20250410_000001, 0x7990_37b3_2193_8a5d_c72f_bccd_95fd_82e5), + (20250410_000002, 0xf2b8_f120_deae_27e7_60d0_79a3_0b77_eea3), + (20250410_000003, 0x06be_fc2b_cedc_acf4_b981_02c7_b40c_c469), + (20250410_000004, 0x0a90_9c6a_dba7_545c_10d9_60eb_6d30_2f50), + (20250410_000006, 0xcc7f_5152_6497_5729_d94b_be0d_9c95_8316), + (20250410_000007, 0x12e7_cfab_a017_a5a5_4f2c_18fa_541c_ce62), + (20250410_000008, 0x171d_62e5_ee1a_f0d9_3639_6c5a_277c_54cd), + (20250410_000009, 0xb1a0_93c7_6645_92ad_df45_b395_57bb_a281), + (20250410_000010, 0x8089_86ac_7cff_8d86_2850_d287_cdb1_2b57), + (20250410_000011, 0x8d9d_3fae_02c9_3d3f_81e4_6242_2b39_b5b8), + (20250410_000012, 0x9805_1372_41aa_d5b0_ebe1_ba9d_28c7_faf6), + (20250410_000013, 0x7291_9a97_e4d1_0d45_1791_6e8c_3f2d_e34d), + (20250410_000014, 0x811d_f965_8127_e168_4aa2_f177_a4e6_f077), + (20250410_000015, 0xa639_0780_aab7_d60d_5fcb_771d_13ed_73ee), + (20250410_000016, 0x22b6_e909_6de4_39e3_b2b9_c684_7417_fe07), + (20250410_000017, 0x9dfe_b6d3_89e4_e509_651b_2793_8d8d_cd32), + (20250410_000018, 0x638f_bdbc_2276_5094_020b_cec1_ab95_c07f), + (20250410_000019, 0xa283_84bc_5fd5_7cbd_b5fb_b5fe_0255_6845), + (20250410_000020, 0x17d1_54b1_7c6e_fc48_61dd_da3d_f8a5_9546), + (20250410_000022, 0xbc36_af82_994a_6f93_8aca_a46b_fc3c_ffde), + (20250410_000023, 0x54ec_3b07_ac79_443b_9e18_a2b3_2d17_5ab9), + (20250410_000024, 0x8ab4_4f80_00b6_58b2_d757_c40f_bc72_3d87), + (20250410_000025, 0x5dc4_2ff3_3042_2f45_046d_10af_ab3a_b583), + (20250410_000026, 0x5263_c547_0b64_6425_5729_48b2_ce84_7cad), + (20250410_000027, 0x0aad_cb50_1d6a_7794_9017_d24d_55e7_1b9d), + (20250410_000028, 0x8fc1_92f8_68df_ca4e_3e2b_cddf_bc12_cffe), + (20250410_000029, 0x416c_9446_b6a3_1b49_2940_a8ac_c1c2_665a), + (20250410_000030, 0x83a5_e51e_25a6_77fb_2b79_6ea5_db1e_364f), + (20250410_000031, 0xfa18_a707_9438_dbc7_2cde_b5f1_ee21_5c7e), + (20250410_000032, 0xd669_662e_8930_838a_b142_c3fa_7b39_d2a0), + (20250410_000033, 0x4019_1053_cabc_191c_c02e_9aa9_407c_0de5), + (20250410_000034, 0xdd59_e595_24e6_4dad_c5f7_fef2_90b8_df57), + (20250410_000035, 0x09b4_ea53_2da4_9c39_eb10_db33_6a6d_608b), + (20250410_000036, 0x3ca5_9c78_8480_e342_d729_907c_d293_2049), + (20250410_000037, 0xc857_2a10_450b_0612_822c_2b86_535a_ea7d), + (20250410_000038, 0x1642_39da_9c3b_d9fd_b1e1_72b1_db78_b978), + (20250410_000039, 0xdd70_b211_6016_bb84_0d84_f04e_eb8a_59d9), + (20250410_000040, 0xe435_ead6_c363_a0b6_e048_dd85_0ecb_9499), + (20250410_000041, 0xe9f3_122f_70d4_9839_c818_4b18_0192_ae26), + (20250410_000043, 0xec5e_1400_483d_c4bf_6014_aba4_ffc3_6236), + (20250410_000044, 0x4750_5eba_4095_6664_78d0_27f9_64bf_64f4), + (20250410_000045, 0x9a53_bd70_4cad_2bf1_61d4_f143_0c82_681d), + (20250410_121612, 0x25f0_9d20_a897_df18_162d_1c47_b68e_81bd), + (20250602_212101, 0xd1a8_782c_b3f0_5045_3f46_49a0_bab0_822b), + (20250708_155857, 0xb78e_6957_a588_c16a_d292_a0c7_cae9_f290), + (20250915_092635, 0x6854_d58b_99d7_3ac5_82f8_25e5_b1c3_cc0b), + (20251127_145951, 0x3bcd_d92e_8391_2a2c_8a18_1d76_354f_96c6), +]; + +fn alternate_checksums_map() -> BTreeMap> { + let mut map = BTreeMap::new(); + for (version, checksum) in ALLOWED_ALTERNATE_CHECKSUMS { + map.entry(*version) + .or_insert_with(HashSet::new) + .insert(*checksum); + } + map +} + +/// Load the list of applied migrations into a map. +/// +/// It's important to use a [`BTreeMap`] so that the migrations are naturally +/// ordered by version. +async fn applied_migrations_map( + conn: &mut PgConnection, +) -> Result, MigrateError> { + let applied_migrations = conn + .list_applied_migrations() + .await? + .into_iter() + .map(|m| (m.version, m)) + .collect(); + + Ok(applied_migrations) +} + +/// Checks if the migration table exists +async fn migration_table_exists(conn: &mut PgConnection) -> Result { + sqlx::query_scalar!( + r#" + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_name = '_sqlx_migrations' + ) AS "exists!" + "#, + ) + .fetch_one(conn) + .await +} + +/// Run the migrations on the given connection +/// +/// This function acquires an advisory lock on the database to ensure that only +/// one migrator is running at a time. +/// +/// # Errors +/// +/// This function returns an error if the migration fails. +#[::tracing::instrument(name = "db.migrate", skip_all, err)] +pub async fn migrate(conn: &mut PgConnection) -> Result<(), MigrateError> { + // Get the database name and use it to derive an advisory lock key. This + // is the same lock key used by SQLx default migrator, so that it works even + // with older versions of MAS, and when running through `cargo sqlx migrate run` + let database_name = sqlx::query_scalar!(r#"SELECT current_database() as "current_database!""#) + .fetch_one(&mut *conn) + .await + .map_err(MigrateError::from)?; + + let lock = + PgAdvisoryLock::with_key(PgAdvisoryLockKey::BigInt(generate_lock_id(&database_name))); + + // Try to acquire the migration lock in a loop. + // + // The reason we do that with a `try_acquire` is because in Postgres, `CREATE + // INDEX CONCURRENTLY` will *not* complete whilst an advisory lock is being + // acquired on another connection. This then means that if we run two + // migration process at the same time, one of them will go through and block + // on concurrent index creations, because the other will get stuck trying to + // acquire this lock. + // + // To avoid this, we use `try_acquire`/`pg_advisory_lock_try` in a loop, which + // will fail immediately if the lock is held by another connection, allowing + // potential 'CREATE INDEX CONCURRENTLY' statements to complete. + let mut backoff = std::time::Duration::from_millis(250); + let mut conn = conn; + let mut locked_connection = loop { + match lock.try_acquire(conn).await? { + Either::Left(guard) => break guard, + Either::Right(conn_) => { + warn!( + "Another process is already running migrations on the database, waiting {duration}s and trying again…", + duration = backoff.as_secs_f32() + ); + tokio::time::sleep(backoff).await; + backoff = std::cmp::min(backoff * 2, std::time::Duration::from_secs(5)); + conn = conn_; + } + } + }; + + // Creates the migration table if missing + // We check if the table exists before calling `ensure_migrations_table` to + // avoid the pesky 'relation "_sqlx_migrations" already exists, skipping' notice + if !migration_table_exists(locked_connection.as_mut()).await? { + locked_connection.as_mut().ensure_migrations_table().await?; + } + + for migration in pending_migrations(locked_connection.as_mut()).await? { + info!( + "Applying migration {version}: {description}", + version = migration.version, + description = migration.description + ); + locked_connection + .as_mut() + .apply(migration) + .instrument(info_span!( + "db.migrate.run_migration", + db.migration.version = migration.version, + db.migration.description = &*migration.description, + { DB_QUERY_TEXT } = &*migration.sql, + )) + .await?; + } + + locked_connection.release_now().await?; + + Ok(()) +} + +/// Get the list of pending migrations +/// +/// # Errors +/// +/// This function returns an error if there is a problem checking the applied +/// migrations +pub async fn pending_migrations( + conn: &mut PgConnection, +) -> Result, MigrateError> { + // Load the maps of available migrations, applied migrations, migrations that + // are allowed to be missing, alternate checksums for migrations that changed + let available_migrations = available_migrations(); + let allowed_missing = allowed_missing_migrations(); + let alternate_checksums = alternate_checksums_map(); + let applied_migrations = if migration_table_exists(&mut *conn).await? { + applied_migrations_map(&mut *conn).await? + } else { + BTreeMap::new() + }; + + // Check that all applied migrations are still valid + for applied_migration in applied_migrations.values() { + // Check that we know about the applied migration + if let Some(migration) = available_migrations.get(&applied_migration.version) { + // Check the migration checksum + if applied_migration.checksum != migration.checksum { + // The checksum we have in the database doesn't match the one we + // have embedded. This might be because a migration was + // intentionally changed, so we check the alternate checksums + if let Some(alternates) = alternate_checksums.get(&applied_migration.version) { + // This converts the first 16 bytes of the checksum into a u128 + let Some(applied_checksum_prefix) = applied_migration + .checksum + .get(..16) + .and_then(|bytes| bytes.try_into().ok()) + .map(u128::from_be_bytes) + else { + return Err(MigrateError::ExecuteMigration( + sqlx::Error::InvalidArgument( + "checksum stored in database is invalid".to_owned(), + ), + applied_migration.version, + )); + }; + + if !alternates.contains(&applied_checksum_prefix) { + warn!( + "The database has a migration applied ({version}) which has known alternative checksums {alternates:x?}, but none of them matched {applied_checksum_prefix:x}", + version = applied_migration.version, + ); + return Err(MigrateError::VersionMismatch(applied_migration.version)); + } + } else { + return Err(MigrateError::VersionMismatch(applied_migration.version)); + } + } + } else if allowed_missing.contains(&applied_migration.version) { + // The migration is missing, but allowed to be missing + debug!( + "The database has a migration applied ({version}) that doesn't exist anymore, but it was intentionally removed", + version = applied_migration.version + ); + } else { + // The migration is missing, warn about it + warn!( + "The database has a migration applied ({version}) that doesn't exist anymore! This should not happen, unless rolling back to an older version of MAS.", + version = applied_migration.version + ); + } + } + + Ok(available_migrations + .values() + .copied() + .filter(|migration| { + !migration.migration_type.is_down_migration() + && !applied_migrations.contains_key(&migration.version) + }) + .collect()) +} + +// Copied from the sqlx source code, so that we generate the same lock ID +fn generate_lock_id(database_name: &str) -> i64 { + const CRC_IEEE: crc::Crc = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + // 0x3d32ad9e chosen by fair dice roll + 0x3d32_ad9e * i64::from(CRC_IEEE.checksum(database_name.as_bytes())) +} diff --git a/crates/storage/src/app_session.rs b/crates/storage/src/app_session.rs index d649ff35e..4c0b7703a 100644 --- a/crates/storage/src/app_session.rs +++ b/crates/storage/src/app_session.rs @@ -196,12 +196,14 @@ pub trait AppSessionRepository: Send + Sync { /// replacing a device). /// /// Should be called *before* creating a new session for the device. + /// + /// Returns true if a session was finished. async fn finish_sessions_to_replace_device( &mut self, clock: &dyn Clock, user: &User, device: &Device, - ) -> Result<(), Self::Error>; + ) -> Result; } repository_impl!(AppSessionRepository: @@ -218,5 +220,5 @@ repository_impl!(AppSessionRepository: clock: &dyn Clock, user: &User, device: &Device, - ) -> Result<(), Self::Error>; + ) -> Result; ); diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index 46ff80c74..d9c1bb019 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -41,6 +41,7 @@ oauth2-types.workspace = true mas-data-model.workspace = true mas-i18n.workspace = true mas-iana.workspace = true +mas-policy.workspace = true mas-router.workspace = true mas-spa.workspace = true diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index f836d7c4b..d43556fae 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -21,13 +21,15 @@ use chrono::{DateTime, Duration, Utc}; use http::{Method, Uri, Version}; use mas_data_model::{ AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState, - DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports, - UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderOnBackchannelLogout, - UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod, User, - UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession, UserRegistration, + DeviceCodeGrant, MatrixUser, UpstreamOAuthLink, UpstreamOAuthProvider, + UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode, + UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode, + UpstreamOAuthProviderTokenAuthMethod, User, UserEmailAuthentication, + UserEmailAuthenticationCode, UserRecoverySession, UserRegistration, }; use mas_i18n::DataLocale; use mas_iana::jose::JsonWebSignatureAlg; +use mas_policy::{Violation, ViolationCode}; use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder}; use oauth2_types::scope::{OPENID, Scope}; use rand::{ @@ -732,6 +734,7 @@ pub struct ConsentContext { grant: AuthorizationGrant, client: Client, action: PostAuthAction, + matrix_user: MatrixUser, } impl TemplateContext for ConsentContext { @@ -755,6 +758,10 @@ impl TemplateContext for ConsentContext { grant, client, action, + matrix_user: MatrixUser { + mxid: "@alice:example.com".to_owned(), + display_name: Some("Alice".to_owned()), + }, } }) .collect(), @@ -765,12 +772,13 @@ impl TemplateContext for ConsentContext { impl ConsentContext { /// Constructs a context for the client consent page #[must_use] - pub fn new(grant: AuthorizationGrant, client: Client) -> Self { + pub fn new(grant: AuthorizationGrant, client: Client, matrix_user: MatrixUser) -> Self { let action = PostAuthAction::continue_grant(grant.id); Self { grant, client, action, + matrix_user, } } } @@ -860,11 +868,50 @@ impl PolicyViolationContext { } } +/// Context used by the `compat_login_policy_violation.html` template +#[derive(Serialize)] +pub struct CompatLoginPolicyViolationContext { + violations: Vec, +} + +impl TemplateContext for CompatLoginPolicyViolationContext { + fn sample( + _now: chrono::DateTime, + _rng: &mut R, + _locales: &[DataLocale], + ) -> BTreeMap + where + Self: Sized, + { + sample_list(vec![ + CompatLoginPolicyViolationContext { violations: vec![] }, + CompatLoginPolicyViolationContext { + violations: vec![Violation { + msg: "user has too many active sessions".to_owned(), + redirect_uri: None, + field: None, + code: Some(ViolationCode::TooManySessions), + }], + }, + ]) + } +} + +impl CompatLoginPolicyViolationContext { + /// Constructs a context for the compatibility login policy violation page + /// given the list of violations + #[must_use] + pub const fn for_violations(violations: Vec) -> Self { + Self { violations } + } +} + /// Context used by the `sso.html` template #[derive(Serialize)] pub struct CompatSsoContext { login: CompatSsoLogin, action: PostAuthAction, + matrix_user: MatrixUser, } impl TemplateContext for CompatSsoContext { @@ -877,23 +924,33 @@ impl TemplateContext for CompatSsoContext { Self: Sized, { let id = Ulid::from_datetime_with_source(now.into(), rng); - sample_list(vec![CompatSsoContext::new(CompatSsoLogin { - id, - redirect_uri: Url::parse("https://app.element.io/").unwrap(), - login_token: "abcdefghijklmnopqrstuvwxyz012345".into(), - created_at: now, - state: CompatSsoLoginState::Pending, - })]) + sample_list(vec![CompatSsoContext::new( + CompatSsoLogin { + id, + redirect_uri: Url::parse("https://app.element.io/").unwrap(), + login_token: "abcdefghijklmnopqrstuvwxyz012345".into(), + created_at: now, + state: CompatSsoLoginState::Pending, + }, + MatrixUser { + mxid: "@alice:example.com".to_owned(), + display_name: Some("Alice".to_owned()), + }, + )]) } } impl CompatSsoContext { /// Constructs a context for the legacy SSO login page #[must_use] - pub fn new(login: CompatSsoLogin) -> Self + pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self where { let action = PostAuthAction::continue_compat_sso_login(login.id); - Self { login, action } + Self { + login, + action, + matrix_user, + } } } @@ -1748,13 +1805,18 @@ impl TemplateContext for DeviceLinkContext { pub struct DeviceConsentContext { grant: DeviceCodeGrant, client: Client, + matrix_user: MatrixUser, } impl DeviceConsentContext { /// Constructs a new context with an existing linked user #[must_use] - pub fn new(grant: DeviceCodeGrant, client: Client) -> Self { - Self { grant, client } + pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self { + Self { + grant, + client, + matrix_user, + } } } @@ -1782,7 +1844,14 @@ impl TemplateContext for DeviceConsentContext { ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)), user_agent: Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()), }; - Self { grant, client } + Self { + grant, + client, + matrix_user: MatrixUser { + mxid: "@alice:example.com".to_owned(), + display_name: Some("Alice".to_owned()), + } + } }) .collect()) } diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index 9d764e032..3a8e3d43d 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -41,6 +41,7 @@ pub fn register( env.add_filter("simplify_url", filter_simplify_url); env.add_filter("add_slashes", filter_add_slashes); env.add_filter("parse_user_agent", filter_parse_user_agent); + env.add_filter("id_color_hash", filter_id_color_hash); env.add_function("add_params_to_url", function_add_params_to_url); env.add_function("counter", || Ok(Value::from_object(Counter::default()))); if let Some(vite_manifest) = vite_manifest { @@ -138,6 +139,12 @@ fn filter_simplify_url(url: &str, kwargs: Kwargs) -> Result u32 { + input.chars().fold(0, |hash, c| hash + c as u32) % 6 + 1 +} + /// Filter which parses a user-agent string fn filter_parse_user_agent(user_agent: String) -> Value { let user_agent = mas_data_model::UserAgent::parse(user_agent); diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 1f9aa3337..60b905550 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -37,14 +37,15 @@ mod macros; pub use self::{ context::{ - AccountInactiveContext, ApiDocContext, AppContext, CompatSsoContext, ConsentContext, - DeviceConsentContext, DeviceLinkContext, DeviceLinkFormField, DeviceNameContext, - EmailRecoveryContext, EmailVerificationContext, EmptyContext, ErrorContext, - FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext, - PasswordRegisterContext, PolicyViolationContext, PostAuthContext, PostAuthContextInner, - RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField, - RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext, - RegisterFormField, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField, + AccountInactiveContext, ApiDocContext, AppContext, CompatLoginPolicyViolationContext, + CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext, + DeviceLinkFormField, DeviceNameContext, EmailRecoveryContext, EmailVerificationContext, + EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, + NotFoundContext, PasswordRegisterContext, PolicyViolationContext, PostAuthContext, + PostAuthContextInner, RecoveryExpiredContext, RecoveryFinishContext, + RecoveryFinishFormField, RecoveryProgressContext, RecoveryStartContext, + RecoveryStartFormField, RegisterContext, RegisterFormField, + RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField, RegisterStepsEmailInUseContext, RegisterStepsRegistrationTokenContext, RegisterStepsRegistrationTokenFormField, RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField, SiteBranding, SiteConfigExt, SiteFeatures, @@ -391,6 +392,9 @@ register_templates! { /// Render the policy violation page pub fn render_policy_violation(WithLanguage>>) { "pages/policy_violation.html" } + /// Render the compatibility login policy violation page + pub fn render_compat_login_policy_violation(WithLanguage>>) { "pages/compat_login_policy_violation.html" } + /// Render the legacy SSO login consent page pub fn render_sso_login(WithLanguage>>) { "pages/sso.html" } diff --git a/deny.toml b/deny.toml index 1671119ca..8fbbe5220 100644 --- a/deny.toml +++ b/deny.toml @@ -19,6 +19,9 @@ ignore = [ # RSA key extraction "Marvin Attack". This is only relevant when using # PKCS#1 v1.5 encryption, which we don't "RUSTSEC-2023-0071", + # This is a newly unmaintained package that we can allow temporarily. + # Remove ASAP once https://github.com/element-hq/matrix-authentication-service/issues/5337 is fixed. + "RUSTSEC-2025-0134", ] [licenses] diff --git a/docs/config.schema.json b/docs/config.schema.json index cda68f145..f6d947e48 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1883,6 +1883,10 @@ "description": "Entrypoint to use when evaluating authorization grants", "type": "string" }, + "compat_login_entrypoint": { + "description": "Entrypoint to use when evaluating compatibility logins", + "type": "string" + }, "password_entrypoint": { "description": "Entrypoint to use when changing password", "type": "string" @@ -2467,6 +2471,10 @@ } ] }, + "skip_confirmation": { + "description": "Whether to skip the interactive screen prompting the user to confirm the\n attributes that are being imported. This requires `localpart.action` to\n be `require` and other attribute actions to be either `ignore`, `force`\n or `require`", + "type": "boolean" + }, "localpart": { "description": "Import the localpart of the MXID", "allOf": [ @@ -2484,7 +2492,7 @@ ] }, "email": { - "description": "Import the email address of the user based on the `email` and\n `email_verified` claims", + "description": "Import the email address of the user", "allOf": [ { "$ref": "#/definitions/EmailImportPreference" @@ -2572,14 +2580,24 @@ "description": "How to handle an existing localpart claim", "oneOf": [ { - "description": "Fails the sso login on conflict", + "description": "Fails the upstream OAuth 2.0 login on conflict", "type": "string", "const": "fail" }, { - "description": "Adds the oauth identity link, regardless of whether there is an existing\n link or not", + "description": "Adds the upstream OAuth 2.0 identity link, regardless of whether there\n is an existing link or not", "type": "string", "const": "add" + }, + { + "description": "Replace any existing upstream OAuth 2.0 identity link", + "type": "string", + "const": "replace" + }, + { + "description": "Adds the upstream OAuth 2.0 identity link *only* if there is no existing\n link for this provider on the matching user", + "type": "string", + "const": "set" } ] }, diff --git a/docs/development/database.md b/docs/development/database.md index 5ffe8a5a8..2deafce2b 100644 --- a/docs/development/database.md +++ b/docs/development/database.md @@ -40,7 +40,7 @@ cargo sqlx prepare ## Migrations -Migration files live in the `migrations` folder in the `mas-core` crate. +Migration files live in the `migrations` folder in the `mas-storage-pg` crate. ```sh cd crates/storage-pg/ # Again, in the mas-storage-pg crate folder @@ -50,3 +50,29 @@ cargo sqlx migrate add [description] # Add new migration files ``` Note that migrations are embedded in the final binary and can be run from the service CLI tool. + +### Removing migrations + +For various reasons, we may want to delete migrations. +In case we do, we *must* declare that migration version as allowed to be missing. +This is because on startup, MAS will validate that all the applied migrations are known, and warn if some are missing. + +To do so, get the migration version and add it to the `ALLOWED_MISSING_MIGRATIONS` array in the `mas-storage-pg` crate. + +### Modifying existing migrations + +We may want to modify existing migrations to fix mistakes. +In case we do, we *must* save the hash of the original migration file so that MAS can validate it on startup. + +To do so, extract the first 16 bytes of the existing applied migration and append it to the `ALLOWED_ALTERNATE_CHECKSUMS` array in the `mas-storage-pg` crate. + +```sql +SELECT version, ENCODE(SUBSTRING(checksum FOR 16), 'hex') AS short_checksum +FROM _sqlx_migrations +WHERE version = 20250410000002; +``` +``` + version | short_checksum +----------------+---------------------------------- + 20250410000002 | f2b8f120deae27e760d079a30b77eea3 +``` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 58637f2d8..d2d65a0b4 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -196,7 +196,7 @@ secrets: # Signing keys keys: - # It needs at least an RSA key to work properly + # At least one RSA key must be configured - key_file: keys/rsa_key - kid: "iv1aShae" key: | @@ -222,7 +222,7 @@ The secret is not updated when the content of the file changes. > Changing the encryption secret afterwards will lead to a loss of all encrypted > information in the database. -### Singing Keys +### Signing Keys The service can use a number of key types for signing. The following key types are supported: @@ -238,9 +238,24 @@ The following key formats are supported: - PKCS#8 PEM or DER-encoded RSA or ECDSA private key, encrypted or not - SEC1 PEM or DER-encoded ECDSA private key +The signing keys are used for: +- signing ID Tokens (as returned in the [Token Endpoint] at `/oauth2/token`); +- signing the response of the [UserInfo Endpoint] at `/oauth2/userinfo` if the + client requests a signed response; +- (niche) signing a JWT for authenticating to an upstream OAuth provider when + the `private_key_jwt` client auth method is configured. + +At a minimum, an RSA key must be configured in order to be compliant with the +[OpenID Connect Core specification][oidc-core-rs256] which specifies the RS256 algorithm +as mandatory to implement by servers for interoperability reasons. + The keys can be given as a directory path via `secrets.keys_dir` or, alternatively, as an inline configuration list via `secrets.keys`. +[Token Endpoint]: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint +[UserInfo Endpoint]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo +[oidc-core-rs256]: https://openid.net/specs/openid-connect-core-1_0.html#ServerMTI + #### `secrets.keys_dir` Path to the directory containing MAS signing key files. @@ -771,6 +786,14 @@ upstream_oauth2: subject: #template: "{{ user.sub }}" + # By default, new users will see a screen confirming the attributes they + # are about to have on their account. + # + # Setting this to `true` allows skipping this screen, but requires the + # `localpart.action` to be set to `require` and the other attributes + # actions to be set to `ignore`, `force` or `require`. + #skip_confirmation: false + # The localpart is the local part of the user's Matrix ID. # For example, on the `example.com` server, if the localpart is `alice`, # the user's Matrix ID will be `@alice:example.com`. @@ -780,8 +803,10 @@ upstream_oauth2: # How to handle when localpart already exists. # Possible values are (default: fail): - # - `add` : Adds the upstream account link to the existing user, regardless of whether there is an existing link or not. # - `fail` : Fails the upstream OAuth 2.0 login. + # - `add` : Adds the upstream account link to the existing user, regardless of whether there is an existing link or not. + # - `replace` : Replace any existing upstream OAuth 2.0 identity link for this provider on the matching user. + # - `set` : Adds the upstream account link *only* if there is no existing link for this provider on the matching user. #on_conflict: fail # The display name is the user's display name. diff --git a/docs/setup/sso.md b/docs/setup/sso.md index 3b1d624e4..15d0212d7 100644 --- a/docs/setup/sso.md +++ b/docs/setup/sso.md @@ -69,20 +69,25 @@ The template has the following variables available: ## Allow linking existing user accounts -The authentication service supports linking external provider identities to existing local user accounts. +The authentication service supports linking external provider identities to existing local user accounts if the `localpart` matches. -To enable this behavior, the following option must be explicitly set in the provider configuration: +If the `localpart` given by the upstream provider matches an existing user and the `claims_imports.localpart.action` is set to `force` or `require`, by default the service will refuse to link to that existing account. +This behaviour is controlled by the `claims_imports.localpart.on_conflict` option, which can be set to: + + * `fail` *(default)*: fails the upstream OAuth 2.0 login + * `add`: automatically adds the upstream account to the existing user, regardless of whether the existing user already has another upstream account or not + * `set`: automatically adds the upstream account to the existing user only if there are no other upstream accounts for that provider linked to the user + * `replace`: automatically replaces any upstream account for that provider linked to the user ```yaml -claims_imports: - localpart: - on_conflict: add +upstream_oauth2: + providers: + - id: … + claims_imports: + localpart: + action: force + on_conflict: set ``` -`on_conflict` configuration is specific to `localpart` claim_imports, it can be either: -* `add` : when a user authenticates with the provider for the first time, the system checks whether a local user already exists with a `localpart` matching the attribute mapping `localpart` , _by default `{{ user.preferred_username }}`_. If a match is found, the external identity is linked to the existing local account. -* `fail` *(default)* : fails the sso login. - -To enable this option, the `localpart` mapping must be set to either `force` or `require`. > ⚠️ **Security Notice** > Enabling this option can introduce a risk of account takeover. diff --git a/frontend/.storybook/locales.ts b/frontend/.storybook/locales.ts index 51c977c63..5b1fd2477 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!v1.7", + projectUrl: "https://localazy.com/p/matrix-authentication-service!v1.8", baseLocale: "en", languages: [ { @@ -181,22 +181,22 @@ const localazyMetadata: LocalazyMetadata = { file: "frontend.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", - "da": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", - "de": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", - "en": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", - "et": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", - "fi": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", - "fr": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", - "hu": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", - "nb_NO": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", - "nl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", - "pl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json", - "pt": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", - "ru": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", - "sv": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", - "uk": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", - "zh#Hans": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" + "cs": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", + "da": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", + "de": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", + "en": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", + "et": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", + "fi": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", + "fr": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", + "hu": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", + "nb_NO": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", + "nl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", + "pl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json", + "pt": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", + "ru": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", + "sv": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", + "uk": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", + "zh#Hans": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" } }, { @@ -204,22 +204,22 @@ const localazyMetadata: LocalazyMetadata = { file: "file.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", - "da": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", - "de": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", - "en": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", - "et": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", - "fi": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", - "fr": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", - "hu": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", - "nb_NO": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", - "nl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", - "pl": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json", - "pt": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", - "ru": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", - "sv": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", - "uk": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", - "zh#Hans": "https://delivery.localazy.com/_a6730564787461928869ed6dbfd8/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" + "cs": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", + "da": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", + "de": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", + "en": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", + "et": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", + "fi": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", + "fr": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", + "hu": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", + "nb_NO": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", + "nl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", + "pl": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json", + "pt": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", + "ru": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", + "sv": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", + "uk": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", + "zh#Hans": "https://delivery.localazy.com/_a6714272761432419575f0da7e87/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" } } ] diff --git a/frontend/locales/de.json b/frontend/locales/de.json index 75c8493ed..e8bf651ca 100644 --- a/frontend/locales/de.json +++ b/frontend/locales/de.json @@ -43,7 +43,7 @@ "alert_description": "Dieses Konto wird dauerhaft entfernt und du hast keinen Zugriff mehr auf deine Nachrichten.", "alert_title": "Du bist kurz davor, alle deine Daten zu verlieren.", "button": "Account löschen", - "dialog_description": "Bestätige, dass du dein Konto löschen möchtest:\n\nDu kannst dein Konto nicht reaktivieren\nDu kannst dich nicht mehr anmelden\nNiemand kann deinen Benutzernamen (MXID) wieder verwenden, auch du nicht.\nDu verlässt alle Gruppen und Chats\nDu wirst vom Identitätsserver entfernt und niemand kann dich mit deiner E-Mail-Adresse oder Telefonnummer finden\n\nDeine alten Nachrichten sind für Empfänger weiterhin sichtbar. Möchtest du deine gesendeten Nachrichten vor zukünftigen Gruppen-Besuchern verbergen?", + "dialog_description": "Bestätige, dass du dein Konto löschen möchtest:\n\n\nDu kannst dein Konto nicht reaktivieren\nDu kannst dich nicht mehr anmelden\nNiemand kann deinen Benutzernamen (MXID) wieder verwenden, auch du nicht.\nDu verlässt alle Gruppen und Chats\nDu wirst vom Identitätsserver entfernt und niemand kann dich mit deiner E-Mail-Adresse oder Telefonnummer finden\n\nDeine alten Nachrichten sind für die jeweiligen Empfänger weiterhin sichtbar. Möchtest du deine gesendeten Nachrichten vor zukünftigen Gruppen-Besuchern verbergen?", "dialog_title": "Dieses Konto löschen?", "erase_checkbox_label": "Ja, alle meine Nachrichten vor neuen Mitgliedern verbergen", "incorrect_password": "Falsches Passwort, versuch's nochmal", diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 731eab2f0..f3028c155 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -319,9 +319,9 @@ "scope": { "edit_profile": "Edit your profile and contact details", "manage_sessions": "Manage your devices and sessions", - "mas_admin": "Administer any user on the matrix-authentication-service", + "mas_admin": "Manage users (urn:mas:admin)", "send_messages": "Send new messages on your behalf", - "synapse_admin": "Administer the Synapse homeserver", + "synapse_admin": "Administer the server (urn:synapse:admin:*)", "view_messages": "View your existing messages and data", "view_profile": "See your profile info and contact details" } diff --git a/frontend/locales/et.json b/frontend/locales/et.json index 086c6ba27..e9f90b09a 100644 --- a/frontend/locales/et.json +++ b/frontend/locales/et.json @@ -391,9 +391,9 @@ "scope": { "edit_profile": "Muuta sinu kasutajaprofiili ning kontaktandmeid", "manage_sessions": "Hallata sinu seadmeid ja sessioone", - "mas_admin": "Hallata iga kasutajat teenuses matrix-authentication-service", + "mas_admin": "Hallata kasutajaid (urn:mas:admin)", "send_messages": "Saata sõnumeid sinu nimel", - "synapse_admin": "Hallata seda Synapse koduserverit", + "synapse_admin": "Hallata seda Synapse koduserverit (urn:synapse:admin:*)", "view_messages": "Vaadata sinu sõnumeid ja andmeid", "view_profile": "Vaadata sinu profiili teavet ja kontaktadmeid" } diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index eb4f0b3f9..d9219d9f9 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -391,9 +391,9 @@ "scope": { "edit_profile": "Modifier votre profil et vos coordonnées", "manage_sessions": "Gérer vos appareils et vos sessions", - "mas_admin": "Administrer n'importe quel utilisateur dans matrix-authentication-service", + "mas_admin": "Administrer les utilisateurs (urn:mas:admin)", "send_messages": "Envoyez de nouveaux messages en votre nom", - "synapse_admin": "Administrer le serveur d’accueil Synapse", + "synapse_admin": "Administrer le serveur (urn:synapse:admin:*)", "view_messages": "Afficher vos messages et données existants", "view_profile": "Voir les informations de votre profil et vos coordonnées" } diff --git a/frontend/locales/nl.json b/frontend/locales/nl.json index e52e20624..15b643f05 100644 --- a/frontend/locales/nl.json +++ b/frontend/locales/nl.json @@ -391,9 +391,9 @@ "scope": { "edit_profile": "Edit your profile and contact details", "manage_sessions": "Manage your devices and sessions", - "mas_admin": "Administer any user on the matrix-authentication-service", + "mas_admin": "Manage users (urn:mas:admin)", "send_messages": "Send new messages on your behalf", - "synapse_admin": "Administer the Synapse homeserver", + "synapse_admin": "Administer the server (urn:synapse:admin:*)", "view_messages": "View your existing messages and data", "view_profile": "See your profile info and contact details" } diff --git a/frontend/locales/uk.json b/frontend/locales/uk.json index e15d27af9..41c945312 100644 --- a/frontend/locales/uk.json +++ b/frontend/locales/uk.json @@ -394,9 +394,9 @@ "scope": { "edit_profile": "Редагування профілю та контактних даних", "manage_sessions": "Керування пристроями та сеансами", - "mas_admin": "Адміністрування будь-якого користувача на matrix-authentication-service", + "mas_admin": "Керування користувачами (urn:mas:admin)", "send_messages": "Надсилати нові повідомлення від вашого імені", - "synapse_admin": "Адміністрування домашнього сервера Synapse", + "synapse_admin": "Адмініструвати сервер (urn:synapse:admin:*)", "view_messages": "Перегляд наявних повідомлень і даних", "view_profile": "Перегляд інформації профілю та контактних даних" } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 76e6c4c9a..121d698ad 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,7 @@ "@fontsource/inter": "^5.2.8", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", - "@tanstack/react-query": "^5.90.10", + "@tanstack/react-query": "^5.90.12", "@tanstack/react-router": "^1.131.44", "@vector-im/compound-design-tokens": "6.4.0", "@vector-im/compound-web": "^8.2.5", @@ -20,44 +20,44 @@ "@zxcvbn-ts/language-common": "^3.0.4", "classnames": "^2.5.1", "date-fns": "^4.1.0", - "i18next": "^25.6.3", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-i18next": "^16.3.5", + "i18next": "^25.7.2", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "react-i18next": "^16.4.1", "swagger-ui-dist": "^5.29.5", - "valibot": "^1.1.0", + "valibot": "^1.2.0", "vaul": "^1.1.2" }, "devDependencies": { - "@biomejs/biome": "^2.3.2", + "@biomejs/biome": "^2.3.8", "@browser-logos/chrome": "^2.0.0", "@browser-logos/firefox": "^3.0.10", "@browser-logos/safari": "^2.1.0", - "@graphql-codegen/cli": "^6.0.2", + "@graphql-codegen/cli": "^6.1.0", "@graphql-codegen/client-preset": "^5.1.1", "@graphql-codegen/typescript-msw": "^3.0.1", - "@storybook/addon-docs": "^10.0.8", - "@storybook/react-vite": "^10.0.8", - "@tanstack/react-query-devtools": "^5.90.2", + "@storybook/addon-docs": "^10.1.4", + "@storybook/react-vite": "^10.1.4", + "@tanstack/react-query-devtools": "^5.91.1", "@tanstack/react-router-devtools": "^1.131.44", "@tanstack/router-plugin": "^1.131.44", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/node": "^24.10.1", - "@types/react": "19.2.6", + "@types/react": "19.2.7", "@types/react-dom": "19.2.3", "@types/swagger-ui-dist": "^3.30.6", - "@vitejs/plugin-react": "^5.1.1", - "@vitest/coverage-v8": "^3.2.4", - "autoprefixer": "^10.4.21", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/coverage-v8": "^4.0.15", + "autoprefixer": "^10.4.22", "browserslist-to-esbuild": "^2.1.1", "graphql": "^16.11.0", "happy-dom": "^20.0.4", - "i18next-cli": "^1.24.20", + "i18next-cli": "^1.30.5", "knip": "^5.66.4", "msw": "^2.11.6", - "msw-storybook-addon": "^2.0.5", + "msw-storybook-addon": "^2.0.6", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "postcss-nesting": "^13.0.2", @@ -66,11 +66,11 @@ "tailwindcss": "^3.4.18", "tinyglobby": "^0.2.15", "typescript": "^5.9.3", - "vite": "7.2.4", + "vite": "7.2.7", "vite-plugin-compression": "^0.5.1", "vite-plugin-graphql-codegen": "^3.7.0", "vite-plugin-manifest-sri": "^0.2.0", - "vitest": "^3.2.4" + "vitest": "^4.0.14" } }, "node_modules/@adobe/css-tools": { @@ -93,20 +93,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@ardatan/relay-compiler": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.3.tgz", @@ -163,6 +149,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1035,9 +1022,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz", - "integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.8.tgz", + "integrity": "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -1051,20 +1038,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.2", - "@biomejs/cli-darwin-x64": "2.3.2", - "@biomejs/cli-linux-arm64": "2.3.2", - "@biomejs/cli-linux-arm64-musl": "2.3.2", - "@biomejs/cli-linux-x64": "2.3.2", - "@biomejs/cli-linux-x64-musl": "2.3.2", - "@biomejs/cli-win32-arm64": "2.3.2", - "@biomejs/cli-win32-x64": "2.3.2" + "@biomejs/cli-darwin-arm64": "2.3.8", + "@biomejs/cli-darwin-x64": "2.3.8", + "@biomejs/cli-linux-arm64": "2.3.8", + "@biomejs/cli-linux-arm64-musl": "2.3.8", + "@biomejs/cli-linux-x64": "2.3.8", + "@biomejs/cli-linux-x64-musl": "2.3.8", + "@biomejs/cli-win32-arm64": "2.3.8", + "@biomejs/cli-win32-x64": "2.3.8" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz", - "integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.8.tgz", + "integrity": "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==", "cpu": [ "arm64" ], @@ -1079,9 +1066,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz", - "integrity": "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.8.tgz", + "integrity": "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==", "cpu": [ "x64" ], @@ -1096,9 +1083,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz", - "integrity": "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.8.tgz", + "integrity": "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==", "cpu": [ "arm64" ], @@ -1113,9 +1100,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz", - "integrity": "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.8.tgz", + "integrity": "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==", "cpu": [ "arm64" ], @@ -1130,9 +1117,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz", - "integrity": "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.8.tgz", + "integrity": "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==", "cpu": [ "x64" ], @@ -1147,9 +1134,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz", - "integrity": "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.8.tgz", + "integrity": "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==", "cpu": [ "x64" ], @@ -1164,9 +1151,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz", - "integrity": "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.8.tgz", + "integrity": "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==", "cpu": [ "arm64" ], @@ -1181,9 +1168,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz", - "integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.8.tgz", + "integrity": "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==", "cpu": [ "x64" ], @@ -1215,6 +1202,23 @@ "integrity": "sha512-diidPiK62E4hlAh0dyLfWQDZXi2SSAGiOuw6iqD1x8ztw7L/Sz3He46FhcxEzYa1hKi1blCkjnKDjqw6rQfgcA==", "dev": true }, + "node_modules/@croct/json": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@croct/json/-/json-2.1.0.tgz", + "integrity": "sha512-UrWfjNQVlBxN+OVcFwHmkjARMW55MBN04E9KfGac8ac8z1QnFVuiOOFtMWXCk3UwsyRqhsNaFoYLZC+xxqsVjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@croct/json5-parser": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@croct/json5-parser/-/json5-parser-0.2.2.tgz", + "integrity": "sha512-0NJMLrbeLbQ0eCVj3UoH/kG2QckUgOASfwmfDTjyW1xAYPyTNJXcWVT/dssJdTJd0pRchW+qF0VFWQHcxs1OVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@croct/json": "^2.1.0" + } + }, "node_modules/@csstools/selector-resolve-nested": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", @@ -1846,6 +1850,7 @@ "resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.8.tgz", "integrity": "sha512-lIZW+WOZYpUH91g9r6rYYhfTmptF3YPPM54ZOs8IYVeeL4SeiAu4tfj7mdr8llYEq31DLYgi6JtGIJa192gB0Q==", "license": "OFL-1.1", + "peer": true, "funding": { "url": "https://github.com/sponsors/ayuhito" } @@ -1855,6 +1860,7 @@ "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", "license": "OFL-1.1", + "peer": true, "funding": { "url": "https://github.com/sponsors/ayuhito" } @@ -1884,18 +1890,18 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/cli": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.0.2.tgz", - "integrity": "sha512-W+0ime0xMrCyG77q+5xiPkkqPLuXJcTx0Zr9TTOxF4zIqWKVsuImS3qVxtpeTx+GRbb8VWv9IedWMtt91JGzQg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.1.0.tgz", + "integrity": "sha512-7w3Zq5IFONVOBcyOiP01Nv9WRxGS/TEaBCAb/ALYA3xHq95dqKCpoGnxt/Ut9R18jiS+aMgT0gc8Tr8sHy44jA==", "dev": true, "license": "MIT", "dependencies": { "@babel/generator": "^7.18.13", "@babel/template": "^7.18.10", "@babel/types": "^7.18.13", - "@graphql-codegen/client-preset": "^5.1.2", + "@graphql-codegen/client-preset": "^5.2.0", "@graphql-codegen/core": "^5.0.0", - "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/plugin-helpers": "^6.1.0", "@graphql-tools/apollo-engine-loader": "^8.0.0", "@graphql-tools/code-file-loader": "^8.0.0", "@graphql-tools/git-loader": "^8.0.0", @@ -2144,21 +2150,21 @@ } }, "node_modules/@graphql-codegen/client-preset": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.1.3.tgz", - "integrity": "sha512-8nlKt8/gO/BovWahLb96taMssHKPibBfslry1ed9DIJtbOrceFYF3yNbFZuTHmI644C7ZvoYK93JkE3VzDlCyg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.2.1.tgz", + "integrity": "sha512-6qFjHQQUWrEH+MVvWs5sPUgme8X+Ivg3WfzaCESooRBQZ4/EnSFlXkPWUTbOKYLRUoMv4g6iTRcZQf6u1wtHZA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^6.0.0", - "@graphql-codegen/gql-tag-operations": "5.0.5", - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/typed-document-node": "^6.1.2", - "@graphql-codegen/typescript": "^5.0.4", - "@graphql-codegen/typescript-operations": "^5.0.4", - "@graphql-codegen/visitor-plugin-common": "^6.1.2", + "@graphql-codegen/gql-tag-operations": "5.1.1", + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/typed-document-node": "^6.1.4", + "@graphql-codegen/typescript": "^5.0.6", + "@graphql-codegen/typescript-operations": "^5.0.6", + "@graphql-codegen/visitor-plugin-common": "^6.2.1", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^10.0.0", "@graphql-typed-document-node/core": "3.2.0", @@ -2211,14 +2217,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.5.tgz", - "integrity": "sha512-DutUBwA3UMOB2AI6O1FDidYw7N0Br4d/ogGrYg6XOeeVuRYigc6i9wX4qiv4ofD34Ujfcfze0U2PI3ZOR33NKw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.1.1.tgz", + "integrity": "sha512-XewD0XxN2sgKieEIFeGWV5yT5X2aNy+eg+K8bHlUD7QfyrN2bi67rv/O5Edu7LVDOJR69uqVBp++18d742mn3Q==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/visitor-plugin-common": "6.1.2", + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "6.2.1", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" @@ -2238,9 +2244,9 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/plugin-helpers": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.0.0.tgz", - "integrity": "sha512-Z7P89vViJvQakRyMbq/JF2iPLruRFOwOB6IXsuSvV/BptuuEd7fsGPuEf8bdjjDxUY0pJZnFN8oC7jIQ8p9GKA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.1.0.tgz", + "integrity": "sha512-JJypehWTcty9kxKiqH7TQOetkGdOYjY78RHlI+23qB59cV2wxjFFVf8l7kmuXS4cpGVUNfIjFhVr7A1W7JMtdA==", "dev": true, "license": "MIT", "dependencies": { @@ -2291,14 +2297,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typed-document-node": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.1.2.tgz", - "integrity": "sha512-DelLv7BY8Sx0toyCiEsc46W3FtqipiiqhprUnGnSalfKnKVB8KUodXKaf70migy6hWyDl5d1OJOp5wrttuIy2Q==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.1.4.tgz", + "integrity": "sha512-ITWsA+qvT7R64z7KmYHXfgyD5ff069FAGq/hpR0EWVfzXT4RW1Xn/3Biw7/jvwMGsS1BTjo8ZLSIMNM8KjE3GA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/visitor-plugin-common": "6.1.2", + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "6.2.1", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", "tslib": "~2.6.0" @@ -2318,15 +2324,15 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-q6S8hX+aR4BzeGgolac4gp22rBnXbLhedmOwT1UBT9e3lGNmNpYC7WJUEzAPjWf6z1lRSNmojLlwEjTnffhKNA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.6.tgz", + "integrity": "sha512-rKW3wYInAnmO/DmKjhW3/KLMxUauUCZuMEPQmuoHChnwIuMjn5kVXCdArGyQqv+vVtFj55aS+sJLN4MPNNjSNg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/plugin-helpers": "^6.1.0", "@graphql-codegen/schema-ast": "^5.0.0", - "@graphql-codegen/visitor-plugin-common": "6.1.2", + "@graphql-codegen/visitor-plugin-common": "6.2.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -2655,15 +2661,15 @@ } }, "node_modules/@graphql-codegen/typescript-operations": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.4.tgz", - "integrity": "sha512-5Bu/BTmyNjdSfSLLBKjC0+4XWcY01uotVcnVIWIxxRdIHoRxnTW6PUkT5CoPHP5r/Uoo3OvIJxh+0LYSH5suwA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.6.tgz", + "integrity": "sha512-pkR/82qWO50OHWeV3BiDuVxNFxiJerpmNjFep71VlabADXiU3GIeSaDd6G9a1/SCniVTXZQk2ivCb0ZJiuwo1A==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", - "@graphql-codegen/typescript": "^5.0.4", - "@graphql-codegen/visitor-plugin-common": "6.1.2", + "@graphql-codegen/plugin-helpers": "^6.1.0", + "@graphql-codegen/typescript": "^5.0.6", + "@graphql-codegen/visitor-plugin-common": "6.2.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -2695,13 +2701,13 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.1.2.tgz", - "integrity": "sha512-zYdrhJKgk8kqE1Xz5/m/Ua42zk+rIvYB/FHh3dE1AhZ6b1IDqgKjF3LnkT+K2qenf9EfT4yNjXd5CEKMeXfHyg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.2.1.tgz", + "integrity": "sha512-5QT1hCV3286mrmoIC7vlFXsTlwELMexhuFIkjh+oVGGL1E8hxkIPAU0kfH/lsPbQHKi8zKmic2pl3tAdyYxNyg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/plugin-helpers": "^6.1.0", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.0.0", "@graphql-tools/utils": "^10.0.0", @@ -3215,14 +3221,14 @@ } }, "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.25", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.25.tgz", - "integrity": "sha512-1S7qq9eyO6ygPNWX2lZd+oxbpl63OhnTTw8+t5OWprM2Tzws9HEosLUpsMR85z1gbezeKtUDt9a2bsSyu4MMFg==", + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.26.tgz", + "integrity": "sha512-cVdS2Hw4hg/WgPVV2wRIzZM975pW5k4vdih3hR4SvEDQVr6MmozmlTQSqzMyi9yg8LKTq540Oz3bYQa286yGmg==", "dev": true, "license": "MIT", "dependencies": { "@ardatan/relay-compiler": "^12.0.3", - "@graphql-tools/utils": "^10.10.3", + "@graphql-tools/utils": "^10.11.0", "tslib": "^2.4.0" }, "engines": { @@ -3278,9 +3284,9 @@ } }, "node_modules/@graphql-tools/utils": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.10.3.tgz", - "integrity": "sha512-2EdYiefeLLxsoeZTukSNZJ0E/Z5NnWBUGK2VJa0DQj1scDhVd93HeT1eW9TszJOYmIh3eWAKLv58ri/1XUmdsQ==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.11.0.tgz", + "integrity": "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3819,16 +3825,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz", @@ -5025,9 +5021,9 @@ "license": "MIT" }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", - "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "dev": true, "license": "MIT" }, @@ -5375,17 +5371,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@storybook/addon-docs": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.0.8.tgz", - "integrity": "sha512-PYuaGXGycsamK/7OrFoE4syHGy22mdqqArl67cfosRwmRxZEI9ManQK0jTjNQM9ZX14NpThMOSWNGoWLckkxog==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.1.4.tgz", + "integrity": "sha512-TWLDJNLS/S3AUyTf9x0Hb8k7d+VWMJCH9dWAS0QenvJG8ga9VaehO6r+e+3YyIDbO1ev3UST3GCjh9SY8tzwRA==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.0.8", - "@storybook/icons": "^1.6.0", - "@storybook/react-dom-shim": "10.0.8", + "@storybook/csf-plugin": "10.1.4", + "@storybook/icons": "^2.0.0", + "@storybook/react-dom-shim": "10.1.4", "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", "ts-dedent": "^2.0.0" @@ -5395,17 +5398,18 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.0.8" + "storybook": "^10.1.4" } }, "node_modules/@storybook/builder-vite": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.0.8.tgz", - "integrity": "sha512-kaf/pUENzXxYgQMHGGPNiIk1ieb+SOMuSeLKx8wAUOlQOrzhtSH+ItACW/l43t+O6YZ8jYHoNBMF1kdQ1+Y5+w==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.1.4.tgz", + "integrity": "sha512-3mUQoCzMuhqAIjj8fdbGlwh+GgHaFpCvU+sxL8kIxnZqflW09SuwM5kS47Y5QDzYbHAPYCPqcBFyJ4EfRuf0rw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.0.8", + "@storybook/csf-plugin": "10.1.4", + "@vitest/mocker": "3.2.4", "ts-dedent": "^2.0.0" }, "funding": { @@ -5413,14 +5417,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.0.8", + "storybook": "^10.1.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.0.8.tgz", - "integrity": "sha512-OtLUWHIm3SDGtclQn6Mdd/YsWizLBgdEBRAdekGtwI/TvICfT7gpWYIycP53v2t9ufu2MIXjsxtV2maZKs8sZg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.1.4.tgz", + "integrity": "sha512-nudIBYx8fBz+1j2Xn1pdfGcgMJ78N/1NFB4MYAxI3YEzxGnQwUjihOO1x3siAXPbjFGmnVHoBx7+6IpO3F70GA==", "dev": true, "license": "MIT", "dependencies": { @@ -5433,7 +5437,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.0.8", + "storybook": "^10.1.4", "vite": "*", "webpack": "*" }, @@ -5460,28 +5464,26 @@ "license": "MIT" }, "node_modules/@storybook/icons": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.6.0.tgz", - "integrity": "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.0.1.tgz", + "integrity": "sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@storybook/react": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.0.8.tgz", - "integrity": "sha512-PkuPb8sAqmjjkowSzm3rutiSuETvZI2F8SnjbHE6FRqZWWK4iFoaUrQbrg5kpPAtX//xIrqkdFwlbmQ3skhiPA==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.1.4.tgz", + "integrity": "sha512-ZBMPdQ99QBv/UtlIZBerDGNsQB30ffxk6twe45FIPutSlKXD6W9r0z7rGa5UWnqmmxa9HjARRhclOFsNGkhs9g==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.0.8" + "@storybook/react-dom-shim": "10.1.4", + "react-docgen": "^8.0.2" }, "funding": { "type": "opencollective", @@ -5490,7 +5492,7 @@ "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", - "storybook": "^10.0.8", + "storybook": "^10.1.4", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -5500,9 +5502,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.0.8.tgz", - "integrity": "sha512-ojuH22MB9Sz6rWbhTmC5IErZr0ZADbZijtPteUdydezY7scORT00UtbNoBcG0V6iVjdChgDtSKw2KHUUfchKqg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.1.4.tgz", + "integrity": "sha512-PARu2HA5nYU1AkioNJNc430pz0oyaHFSSAdN3NEaWwkoGrCOo9ZpAXP9V7wlJANCi1pndbC84gSuHVnBXJBG6g==", "dev": true, "license": "MIT", "funding": { @@ -5512,20 +5514,20 @@ "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", - "storybook": "^10.0.8" + "storybook": "^10.1.4" } }, "node_modules/@storybook/react-vite": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.0.8.tgz", - "integrity": "sha512-HS2X4qlitrZr3/sN2+ollxAaNE813IasZRE8lOez1Ey1ISGBtYIb9rmJs82MK35+yDM0pHdiDjkFMD4SkNYh2g==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.1.4.tgz", + "integrity": "sha512-PneYbxBGArczDtDAvQu6Ug5oeDYM5SQiEDSF0i+TNN0ZKO2ROsmbGSI9/7YTFontXR2CqweIO8GyOGQOcz5K9A==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.0.8", - "@storybook/react": "10.0.8", + "@storybook/builder-vite": "10.1.4", + "@storybook/react": "10.1.4", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", @@ -5539,14 +5541,14 @@ "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", - "storybook": "^10.0.8", + "storybook": "^10.1.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@swc/core": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.2.tgz", - "integrity": "sha512-OQm+yJdXxvSjqGeaWhP6Ia264ogifwAO7Q12uTDVYj/Ks4jBTI4JknlcjDRAXtRhqbWsfbZyK/5RtuIPyptk3w==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", + "integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -5562,16 +5564,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.2", - "@swc/core-darwin-x64": "1.15.2", - "@swc/core-linux-arm-gnueabihf": "1.15.2", - "@swc/core-linux-arm64-gnu": "1.15.2", - "@swc/core-linux-arm64-musl": "1.15.2", - "@swc/core-linux-x64-gnu": "1.15.2", - "@swc/core-linux-x64-musl": "1.15.2", - "@swc/core-win32-arm64-msvc": "1.15.2", - "@swc/core-win32-ia32-msvc": "1.15.2", - "@swc/core-win32-x64-msvc": "1.15.2" + "@swc/core-darwin-arm64": "1.15.3", + "@swc/core-darwin-x64": "1.15.3", + "@swc/core-linux-arm-gnueabihf": "1.15.3", + "@swc/core-linux-arm64-gnu": "1.15.3", + "@swc/core-linux-arm64-musl": "1.15.3", + "@swc/core-linux-x64-gnu": "1.15.3", + "@swc/core-linux-x64-musl": "1.15.3", + "@swc/core-win32-arm64-msvc": "1.15.3", + "@swc/core-win32-ia32-msvc": "1.15.3", + "@swc/core-win32-x64-msvc": "1.15.3" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -5583,9 +5585,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.2.tgz", - "integrity": "sha512-Ghyz4RJv4zyXzrUC1B2MLQBbppIB5c4jMZJybX2ebdEQAvryEKp3gq1kBksCNsatKGmEgXul88SETU19sMWcrw==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz", + "integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==", "cpu": [ "arm64" ], @@ -5600,9 +5602,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.2.tgz", - "integrity": "sha512-7n/PGJOcL2QoptzL42L5xFFfXY5rFxLHnuz1foU+4ruUTG8x2IebGhtwVTpaDN8ShEv2UZObBlT1rrXTba15Zw==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz", + "integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==", "cpu": [ "x64" ], @@ -5617,9 +5619,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.2.tgz", - "integrity": "sha512-ZUQVCfRJ9wimuxkStRSlLwqX4TEDmv6/J+E6FicGkQ6ssLMWoKDy0cAo93HiWt/TWEee5vFhFaSQYzCuBEGO6A==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz", + "integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==", "cpu": [ "arm" ], @@ -5634,9 +5636,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.2.tgz", - "integrity": "sha512-GZh3pYBmfnpQ+JIg+TqLuz+pM+Mjsk5VOzi8nwKn/m+GvQBsxD5ectRtxuWUxMGNG8h0lMy4SnHRqdK3/iJl7A==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz", + "integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==", "cpu": [ "arm64" ], @@ -5651,9 +5653,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.2.tgz", - "integrity": "sha512-5av6VYZZeneiYIodwzGMlnyVakpuYZryGzFIbgu1XP8wVylZxduEzup4eP8atiMDFmIm+s4wn8GySJmYqeJC0A==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz", + "integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==", "cpu": [ "arm64" ], @@ -5668,9 +5670,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.2.tgz", - "integrity": "sha512-1nO/UfdCLuT/uE/7oB3EZgTeZDCIa6nL72cFEpdegnqpJVNDI6Qb8U4g/4lfVPkmHq2lvxQ0L+n+JdgaZLhrRA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz", + "integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==", "cpu": [ "x64" ], @@ -5685,9 +5687,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.2.tgz", - "integrity": "sha512-Ksfrb0Tx310kr+TLiUOvB/I80lyZ3lSOp6cM18zmNRT/92NB4mW8oX2Jo7K4eVEI2JWyaQUAFubDSha2Q+439A==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz", + "integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==", "cpu": [ "x64" ], @@ -5702,9 +5704,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.2.tgz", - "integrity": "sha512-IzUb5RlMUY0r1A9IuJrQ7Tbts1wWb73/zXVXT8VhewbHGoNlBKE0qUhKMED6Tv4wDF+pmbtUJmKXDthytAvLmg==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz", + "integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==", "cpu": [ "arm64" ], @@ -5719,9 +5721,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.2.tgz", - "integrity": "sha512-kCATEzuY2LP9AlbU2uScjcVhgnCAkRdu62vbce17Ro5kxEHxYWcugkveyBRS3AqZGtwAKYbMAuNloer9LS/hpw==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz", + "integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==", "cpu": [ "ia32" ], @@ -5736,9 +5738,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.2.tgz", - "integrity": "sha512-iJaHeYCF4jTn7OEKSa3KRiuVFIVYts8jYjNmCdyz1u5g8HRyTDISD76r8+ljEOgm36oviRQvcXaw6LFp1m0yyA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz", + "integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==", "cpu": [ "x64" ], @@ -5783,9 +5785,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.90.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz", - "integrity": "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==", + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz", + "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==", "license": "MIT", "funding": { "type": "github", @@ -5793,9 +5795,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.90.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", - "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==", + "version": "5.91.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.91.1.tgz", + "integrity": "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==", "dev": true, "license": "MIT", "funding": { @@ -5804,12 +5806,13 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.10", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz", - "integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==", + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz", + "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==", "license": "MIT", + "peer": true, "dependencies": { - "@tanstack/query-core": "5.90.10" + "@tanstack/query-core": "5.90.12" }, "funding": { "type": "github", @@ -5820,20 +5823,20 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz", - "integrity": "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==", + "version": "5.91.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.1.tgz", + "integrity": "sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.90.1" + "@tanstack/query-devtools": "5.91.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query": "^5.90.10", "react": "^18 || ^19" } }, @@ -5842,6 +5845,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.131.44.tgz", "integrity": "sha512-LREJfrl8lSedXHCRAAt0HvnHFP9ikAQWnVhYRM++B26w4ZYQBbLvgCT1BCDZVY7MR6rslcd4OfgpZMOyVhNzFg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/react-store": "^0.7.0", @@ -5937,6 +5941,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.44.tgz", "integrity": "sha512-Npi9xB3GSYZhRW8+gPhP6bEbyx0vNc8ZNwsi0JapdiFpIiszgRJ57pesy/rklruv46gYQjLVA5KDOsuaCT/urA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/store": "^0.7.0", @@ -6203,8 +6208,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6295,16 +6299,18 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/@types/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", - "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6315,6 +6321,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6362,6 +6369,7 @@ "resolved": "https://registry.npmjs.org/@vector-im/compound-design-tokens/-/compound-design-tokens-6.4.0.tgz", "integrity": "sha512-93nYQZMgUt6apjCwwnMhMxN8VYQXN3GYOnwovwJjavImwsCGwI/e853BV/DstrWumYh6k5pZsP9e6AF+nz3SIQ==", "license": "SEE LICENSE IN README.md", + "peer": true, "peerDependencies": { "@types/react": "*", "react": "^17 || ^18 || ^19.0.0" @@ -6405,16 +6413,16 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", - "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.47", + "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -6426,32 +6434,30 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.15.tgz", + "integrity": "sha512-FUJ+1RkpTFW7rQITdgTi93qOCWJobWhBirEPCeXh2SW2wsTlFxy51apDz5gzG+ZEYt/THvWeNmhdAoS9DTwpCw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", + "@vitest/utils": "4.0.15", + "ast-v8-to-istanbul": "^0.3.8", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" + "@vitest/browser": "4.0.15", + "vitest": "4.0.15" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6459,6 +6465,43 @@ } } }, + "node_modules/@vitest/coverage-v8/node_modules/@vitest/pretty-format": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz", + "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/@vitest/utils": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz", + "integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -6527,35 +6570,94 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz", + "integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.0.15", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz", + "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/utils": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz", + "integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz", + "integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz", + "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/spy": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", @@ -6845,13 +6947,13 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.5.tgz", - "integrity": "sha512-9SdXjNheSiE8bALAQCQQuT6fgQaoxJh7IRYrRGZ8/9nv8WhJeC1aXAwN8TbaOssGOukUvyvnkgD9+Yuykvl1aA==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.30", + "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } @@ -6887,9 +6989,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "dev": true, "funding": [ { @@ -6907,9 +7009,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -6991,9 +7093,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.2.tgz", - "integrity": "sha512-NvcIedLxrs9llVpX7wI+Jz4Hn9vJQkCPKrTaHIE0sW/Rj1iq6Fzby4NbyTZjQJNoypBXNaG7tEHkTgONZpwgxQ==", + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7037,9 +7139,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -7056,12 +7158,13 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.2", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -7099,16 +7202,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7151,9 +7244,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", "dev": true, "funding": [ { @@ -7619,7 +7712,8 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -7790,8 +7884,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dot-case": { "version": "3.0.4", @@ -7822,9 +7915,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -7892,6 +7985,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7976,9 +8070,9 @@ "license": "MIT" }, "node_modules/execa": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", - "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", "dev": true, "license": "MIT", "dependencies": { @@ -8222,16 +8316,16 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -8417,6 +8511,7 @@ "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -8610,9 +8705,9 @@ } }, "node_modules/i18next": { - "version": "25.6.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.3.tgz", - "integrity": "sha512-AEQvoPDljhp67a1+NsnG/Wb1Nh6YoSvtrmeEd24sfGn3uujCtXCF3cXpr7ulhMywKNFF7p3TX1u2j7y+caLOJg==", + "version": "25.7.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.2.tgz", + "integrity": "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw==", "funding": [ { "type": "individual", @@ -8628,6 +8723,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -8641,19 +8737,20 @@ } }, "node_modules/i18next-cli": { - "version": "1.24.20", - "resolved": "https://registry.npmjs.org/i18next-cli/-/i18next-cli-1.24.20.tgz", - "integrity": "sha512-WpHJD24Ghi3s0/nEL2Bv/RUeaflOWEGIdwLkg2QWwQdxCMZCRZzXkRcT2qQzjipUPwjz17Kfe5P8ke8pw0tLJg==", + "version": "1.30.5", + "resolved": "https://registry.npmjs.org/i18next-cli/-/i18next-cli-1.30.5.tgz", + "integrity": "sha512-1Ls80XXcwlsNhsFsWYMZV9ik1fPMdCny+hF8w64UkLLUdwxuUc/F8Vi7xbMv9oy3ODd6YBwNQwbKdH+5e6F9vA==", "dev": true, "license": "MIT", "dependencies": { - "@swc/core": "1.15.2", + "@croct/json5-parser": "0.2.2", + "@swc/core": "1.15.3", "chalk": "5.6.2", "chokidar": "4.0.3", "commander": "14.0.2", - "execa": "9.6.0", - "glob": "12.0.0", - "i18next-resources-for-ts": "1.8.0", + "execa": "9.6.1", + "glob": "13.0.0", + "i18next-resources-for-ts": "1.9.0", "inquirer": "12.10.0", "jiti": "2.6.1", "jsonc-parser": "3.3.1", @@ -8708,38 +8805,16 @@ } }, "node_modules/i18next-cli/node_modules/glob": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", - "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/i18next-cli/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, "engines": { "node": "20 || >=22" }, @@ -8758,11 +8833,11 @@ } }, "node_modules/i18next-cli/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -8815,44 +8890,45 @@ } }, "node_modules/i18next-resources-for-ts": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-1.8.0.tgz", - "integrity": "sha512-I/qMaoARO2WBPjFYumh0Ceu1Bj9GckdxMAFFcAz9EtAFNPGybbeZNKluVPIN6iqMQatuJQ4XHagvx/C/gjqBCQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-1.9.0.tgz", + "integrity": "sha512-P5kZmxCVKVdiJU0z6Nf+3qW3sdN4TlxDRsuyiuLT5SdM1Yy8yqUgR6JlNZPAd1VWMaXp8aiauK8EuOi3XuKhcw==", "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "chokidar": "^4.0.3", - "yaml": "^2.8.1" + "@swc/core": "^1.15.3", + "chokidar": "^5.0.0", + "yaml": "^2.8.2" }, "bin": { "i18next-resources-for-ts": "bin/i18next-resources-for-ts.js" } }, "node_modules/i18next-resources-for-ts/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/i18next-resources-for-ts/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -9779,15 +9855,14 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9795,15 +9870,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { @@ -9986,6 +10061,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", @@ -10025,9 +10101,9 @@ } }, "node_modules/msw-storybook-addon": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.5.tgz", - "integrity": "sha512-uum2gtprDBoUb8GV/rPMwPytHmB8+AUr25BQUY0MpjYey5/ujaew2Edt+4oHiXpLTd0ThyMqmEvGy/sRpDV4lg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.6.tgz", + "integrity": "sha512-ExCwDbcJoM2V3iQU+fZNp+axVfNc7DWMRh4lyTXebDO8IbpUNYKGFUrA8UqaeWiRGKVuS7+fU+KXEa9b0OP6uA==", "dev": true, "license": "MIT", "dependencies": { @@ -10152,9 +10228,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -10235,6 +10311,17 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10706,6 +10793,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10863,6 +10951,7 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10900,7 +10989,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -10916,7 +11004,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -10972,18 +11059,19 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-docgen": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.1.tgz", - "integrity": "sha512-kQKsqPLplY3Hx4jGnM3jpQcG3FQDt7ySz32uTHt3C9HAe45kNXG+3o16Eqn3Fw1GtMfHoN3b4J/z2e6cZJCmqQ==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.2.tgz", + "integrity": "sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA==", "dev": true, "license": "MIT", "dependencies": { @@ -11013,21 +11101,22 @@ } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.1" } }, "node_modules/react-i18next": { - "version": "16.3.5", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.5.tgz", - "integrity": "sha512-F7Kglc+T0aE6W2rO5eCAFBEuWRpNb5IFmXOYEgztjZEuiuSLTe/xBIEG6Q3S0fbl8GXMNo+Q7gF8bpokFNWJww==", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.4.1.tgz", + "integrity": "sha512-GzsYomxb1/uE7nlJm0e1qQ8f+W9I3Xirh9VoycZIahk6C8Pmv/9Fd0ek6zjf1FSgtGLElDGqwi/4FOHEGUbsEQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.27.6", @@ -11056,8 +11145,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.18.0", @@ -11473,6 +11561,7 @@ "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -11592,6 +11681,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -11825,9 +11915,9 @@ } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -11845,22 +11935,23 @@ } }, "node_modules/storybook": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.0.8.tgz", - "integrity": "sha512-vQMufKKA9TxgoEDHJv3esrqUkjszuuRiDkThiHxENFPdQawHhm2Dei+iwNRwH5W671zTDy9iRT9P1KDjcU5Iyw==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.1.4.tgz", + "integrity": "sha512-FrBjm8I8O+pYEOPHcdW9xWwgXSZxte7lza9q2lN3jFN4vuW79m5j0OnTQeR8z9MmIbBTvkIpp3yMBebl53Yt5Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.6.0", + "@storybook/icons": "^2.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", "@vitest/spy": "3.2.4", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "recast": "^0.23.5", "semver": "^7.6.2", + "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "bin": { @@ -12013,26 +12104,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -12257,21 +12328,6 @@ "node": ">=4" } }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -12309,7 +12365,8 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tiny-warning": { "version": "1.0.3", @@ -12325,11 +12382,14 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -12348,16 +12408,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", @@ -12512,6 +12562,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12640,9 +12691,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "dev": true, "funding": [ { @@ -12757,9 +12808,9 @@ "license": "MIT" }, "node_modules/valibot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", - "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -12784,11 +12835,12 @@ } }, "node_modules/vite": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", - "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -12858,29 +12910,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-compression": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz", @@ -12931,51 +12960,51 @@ "license": "MIT" }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz", + "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.0.15", + "@vitest/mocker": "4.0.15", + "@vitest/pretty-format": "4.0.15", + "@vitest/runner": "4.0.15", + "@vitest/snapshot": "4.0.15", + "@vitest/spy": "4.0.15", + "@vitest/utils": "4.0.15", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.15", + "@vitest/browser-preview": "4.0.15", + "@vitest/browser-webdriverio": "4.0.15", + "@vitest/ui": "4.0.15", "happy-dom": "*", "jsdom": "*" }, @@ -12983,13 +13012,19 @@ "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { "optional": true }, "@vitest/ui": { @@ -13003,6 +13038,118 @@ } } }, + "node_modules/vitest/node_modules/@vitest/expect": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz", + "integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.15", + "@vitest/utils": "4.0.15", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz", + "integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.15", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/pretty-format": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz", + "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/spy": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz", + "integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/utils": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz", + "integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -13154,6 +13301,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -13188,9 +13336,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", "bin": { @@ -13198,6 +13346,9 @@ }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/frontend/package.json b/frontend/package.json index f3fbe5c4b..78a21c4fa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "@fontsource/inter": "^5.2.8", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", - "@tanstack/react-query": "^5.90.10", + "@tanstack/react-query": "^5.90.12", "@tanstack/react-router": "^1.131.44", "@vector-im/compound-design-tokens": "6.4.0", "@vector-im/compound-web": "^8.2.5", @@ -30,44 +30,44 @@ "@zxcvbn-ts/language-common": "^3.0.4", "classnames": "^2.5.1", "date-fns": "^4.1.0", - "i18next": "^25.6.3", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-i18next": "^16.3.5", + "i18next": "^25.7.2", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "react-i18next": "^16.4.1", "swagger-ui-dist": "^5.29.5", - "valibot": "^1.1.0", + "valibot": "^1.2.0", "vaul": "^1.1.2" }, "devDependencies": { - "@biomejs/biome": "^2.3.2", + "@biomejs/biome": "^2.3.8", "@browser-logos/chrome": "^2.0.0", "@browser-logos/firefox": "^3.0.10", "@browser-logos/safari": "^2.1.0", - "@graphql-codegen/cli": "^6.0.2", + "@graphql-codegen/cli": "^6.1.0", "@graphql-codegen/client-preset": "^5.1.1", "@graphql-codegen/typescript-msw": "^3.0.1", - "@storybook/addon-docs": "^10.0.8", - "@storybook/react-vite": "^10.0.8", - "@tanstack/react-query-devtools": "^5.90.2", + "@storybook/addon-docs": "^10.1.4", + "@storybook/react-vite": "^10.1.4", + "@tanstack/react-query-devtools": "^5.91.1", "@tanstack/react-router-devtools": "^1.131.44", "@tanstack/router-plugin": "^1.131.44", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/node": "^24.10.1", - "@types/react": "19.2.6", + "@types/react": "19.2.7", "@types/react-dom": "19.2.3", "@types/swagger-ui-dist": "^3.30.6", - "@vitejs/plugin-react": "^5.1.1", - "@vitest/coverage-v8": "^3.2.4", - "autoprefixer": "^10.4.21", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/coverage-v8": "^4.0.15", + "autoprefixer": "^10.4.22", "browserslist-to-esbuild": "^2.1.1", "graphql": "^16.11.0", "happy-dom": "^20.0.4", - "i18next-cli": "^1.24.20", + "i18next-cli": "^1.30.5", "knip": "^5.66.4", "msw": "^2.11.6", - "msw-storybook-addon": "^2.0.5", + "msw-storybook-addon": "^2.0.6", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "postcss-nesting": "^13.0.2", @@ -76,11 +76,11 @@ "tailwindcss": "^3.4.18", "tinyglobby": "^0.2.15", "typescript": "^5.9.3", - "vite": "7.2.4", + "vite": "7.2.7", "vite-plugin-compression": "^0.5.1", "vite-plugin-graphql-codegen": "^3.7.0", "vite-plugin-manifest-sri": "^0.2.0", - "vitest": "^3.2.4" + "vitest": "^4.0.14" }, "msw": { "workerDirectory": [ diff --git a/frontend/src/entrypoints/templates.css b/frontend/src/entrypoints/templates.css index 5dfe5986d..8e4870bc7 100644 --- a/frontend/src/entrypoints/templates.css +++ b/frontend/src/entrypoints/templates.css @@ -23,9 +23,9 @@ letter-spacing: var(--cpd-font-letter-spacing-body-lg); } -.cpd-text-heading-xl-semibold { - font: var(--cpd-font-heading-xl-semibold); - letter-spacing: var(--cpd-font-letter-spacing-heading-xl); +.cpd-text-body-lg-semibold { + font: var(--cpd-font-body-lg-semibold); + letter-spacing: var(--cpd-font-letter-spacing-body-lg); } .cpd-text-body-md-regular { @@ -33,6 +33,36 @@ letter-spacing: var(--cpd-font-letter-spacing-body-md); } +.cpd-text-body-md-semibold { + font: var(--cpd-font-body-md-semibold); + letter-spacing: var(--cpd-font-letter-spacing-body-md); +} + +.cpd-text-body-sm-regular { + font: var(--cpd-font-body-sm-regular); + letter-spacing: var(--cpd-font-letter-spacing-body-sm); +} + +.cpd-text-body-sm-semibold { + font: var(--cpd-font-body-sm-semibold); + letter-spacing: var(--cpd-font-letter-spacing-body-sm); +} + +.cpd-text-body-xs-regular { + font: var(--cpd-font-body-xs-regular); + letter-spacing: var(--cpd-font-letter-spacing-body-xs); +} + +.cpd-text-body-xs-semibold { + font: var(--cpd-font-body-xs-semibold); + letter-spacing: var(--cpd-font-letter-spacing-body-xs); +} + +.cpd-text-heading-xl-semibold { + font: var(--cpd-font-heading-xl-semibold); + letter-spacing: var(--cpd-font-letter-spacing-heading-xl); +} + .cpd-text-primary { color: var(--cpd-color-text-primary); } @@ -186,3 +216,48 @@ } } } + +.avatar-placeholder { + height: var(--cpd-space-14x); + width: var(--cpd-space-14x); + border-radius: 50%; + overflow: hidden; + user-select: none; + line-height: var(--cpd-space-14x); + font-size: 32px; + font-family: var(--cpd-font-family-sans); + font-weight: bold; + text-align: center; + background-color: var(--cpd-avatar-bg); + color: var(--cpd-avatar-color); + + &[data-color] { + --cpd-avatar-bg: var(--cpd-color-bg-decorative-1); + --cpd-avatar-color: var(--cpd-color-text-decorative-1); + } + + &[data-color="2"] { + --cpd-avatar-bg: var(--cpd-color-bg-decorative-2); + --cpd-avatar-color: var(--cpd-color-text-decorative-2); + } + + &[data-color="3"] { + --cpd-avatar-bg: var(--cpd-color-bg-decorative-3); + --cpd-avatar-color: var(--cpd-color-text-decorative-3); + } + + &[data-color="4"] { + --cpd-avatar-bg: var(--cpd-color-bg-decorative-4); + --cpd-avatar-color: var(--cpd-color-text-decorative-4); + } + + &[data-color="5"] { + --cpd-avatar-bg: var(--cpd-color-bg-decorative-5); + --cpd-avatar-color: var(--cpd-color-text-decorative-5); + } + + &[data-color="6"] { + --cpd-avatar-bg: var(--cpd-color-bg-decorative-6); + --cpd-avatar-color: var(--cpd-color-text-decorative-6); + } +} diff --git a/frontend/src/test-utils/mockLocale.ts b/frontend/src/test-utils/mockLocale.ts index 2eb89ce60..e42b5af08 100644 --- a/frontend/src/test-utils/mockLocale.ts +++ b/frontend/src/test-utils/mockLocale.ts @@ -12,11 +12,31 @@ import { vi } from "vitest"; * Defaults to `en-GB` */ export const mockLocale = (defaultLocale = "en-GB"): void => { - const { DateTimeFormat } = Intl; + const OriginalDateTimeFormat = Intl.DateTimeFormat; + + // Vitest 4.x requires function/class implementations for spyOn mocks when + // mocking constructors. For built-in constructors like Intl.DateTimeFormat + // that have internal slots, we use a function that returns a new instance. + // This is valid JavaScript - when a constructor returns an object, that + // object becomes the instance (instead of `this`). + function MockDateTimeFormat( + this: unknown, + locales?: Intl.LocalesArgument, + options?: Intl.DateTimeFormatOptions, + ): Intl.DateTimeFormat { + // Apply default locale when no locale is specified + return new OriginalDateTimeFormat(locales || defaultLocale, options); + } + + // Inherit static methods from the original DateTimeFormat + Object.setPrototypeOf(MockDateTimeFormat, OriginalDateTimeFormat); + // Set up prototype chain so instanceof checks work correctly + Object.setPrototypeOf( + MockDateTimeFormat.prototype, + OriginalDateTimeFormat.prototype, + ); + vi.spyOn(Intl, "DateTimeFormat").mockImplementation( - ( - locales?: Intl.LocalesArgument, - options?: Intl.DateTimeFormatOptions | undefined, - ) => new DateTimeFormat(locales || defaultLocale, options), + MockDateTimeFormat as typeof Intl.DateTimeFormat, ); }; diff --git a/policies/Makefile b/policies/Makefile index 0d515b904..db5991672 100644 --- a/policies/Makefile +++ b/policies/Makefile @@ -16,6 +16,7 @@ INPUTS := \ client_registration/client_registration.rego \ register/register.rego \ authorization_grant/authorization_grant.rego \ + compat_login/compat_login.rego \ email/email.rego ifeq ($(DOCKER), 1) @@ -38,6 +39,7 @@ policy.wasm: $(INPUTS) -e "client_registration/violation" \ -e "register/violation" \ -e "authorization_grant/violation" \ + -e "compat_login/violation" \ -e "email/violation" \ $^ tar xzf bundle.tar.gz /policy.wasm diff --git a/policies/compat_login/compat_login.rego b/policies/compat_login/compat_login.rego new file mode 100644 index 000000000..4f76842cd --- /dev/null +++ b/policies/compat_login/compat_login.rego @@ -0,0 +1,74 @@ +# Copyright 2025 Element Creations Ltd. +# +# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +# Please see LICENSE files in the repository root for full details. + +# METADATA +# schemas: +# - input: schema["compat_login_input"] +package compat_login + +import rego.v1 + +import data.common + +default allow := false + +allow if { + count(violation) == 0 +} + +violation contains {"msg": sprintf( + "Requester [%s] isn't allowed to do this action", + [common.format_requester(input.requester)], +)} if { + common.requester_banned(input.requester, data.requester) +} + +violation contains { + "code": "too-many-sessions", + "msg": "user has too many active sessions (soft limit)", +} if { + # Only apply if session limits are enabled in the config + data.session_limit != null + + # This is a web-based interactive login + is_interactive + + # Only apply if this login doesn't replace a session + # (As then this login is not actually increasing the number of devices) + not input.session_replaced + + # For web-based 'compat SSO' login, a violation occurs when the soft limit has already been + # reached or exceeded. + # We use the soft limit because the user will be able to interactively remove + # sessions to return under the limit. + data.session_limit.soft_limit <= input.session_counts.total +} + +violation contains { + "code": "too-many-sessions", + "msg": "user has too many active sessions (hard limit)", +} if { + # Only apply if session limits are enabled in the config + data.session_limit != null + + # This is not a web-based interactive login + not is_interactive + + # Only apply if this login doesn't replace a session + # (As then this login is not actually increasing the number of devices) + not input.session_replaced + + # For `m.login.password` login, a violation occurs when the hard limit has already been + # reached or exceeded. + # We don't use the soft limit because the user won't be able to interactively remove + # sessions to return under the limit. + data.session_limit.hard_limit <= input.session_counts.total +} + +is_interactive if { + # Only `m.login.sso` (the interactive web form) is interactive; + # `m.login.password` and `m.login.token` (including the finalisation of an SSO login) are not + input.login.type == "m.login.sso" +} diff --git a/policies/compat_login/compat_login_test.rego b/policies/compat_login/compat_login_test.rego new file mode 100644 index 000000000..1b8049844 --- /dev/null +++ b/policies/compat_login/compat_login_test.rego @@ -0,0 +1,99 @@ +# Copyright 2025 Element Creations Ltd. +# +# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +# Please see LICENSE files in the repository root for full details. + +package compat_login_test + +import data.compat_login +import rego.v1 + +user := {"username": "john"} + +# Tests session limiting when using (the interactive part of) `m.login.sso` +test_session_limiting_sso if { + compat_login.allow with input.user as user + with input.session_counts as {"total": 1} + with input.login as {"type": "m.login.sso"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + compat_login.allow with input.user as user + with input.session_counts as {"total": 31} + with input.login as {"type": "m.login.sso"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + not compat_login.allow with input.user as user + with input.session_counts as {"total": 32} + with input.login as {"type": "m.login.sso"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + not compat_login.allow with input.user as user + with input.session_counts as {"total": 42} + with input.login as {"type": "m.login.sso"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + not compat_login.allow with input.user as user + with input.session_counts as {"total": 65} + with input.login as {"type": "m.login.sso"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + # No limit configured + compat_login.allow with input.user as user + with input.session_counts as {"total": 1} + with input.login as {"type": "m.login.sso"} + with input.session_replaced as false + with data.session_limit as null +} + +# Test session limiting when using `m.login.password` +test_session_limiting_password if { + compat_login.allow with input.user as user + with input.session_counts as {"total": 1} + with input.login as {"type": "m.login.password"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + compat_login.allow with input.user as user + with input.session_counts as {"total": 63} + with input.login as {"type": "m.login.password"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + not compat_login.allow with input.user as user + with input.session_counts as {"total": 64} + with input.login as {"type": "m.login.password"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + not compat_login.allow with input.user as user + with input.session_counts as {"total": 65} + with input.login as {"type": "m.login.password"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + # No limit configured + compat_login.allow with input.user as user + with input.session_counts as {"total": 1} + with input.login as {"type": "m.login.password"} + with input.session_replaced as false + with data.session_limit as null +} + +test_no_session_limiting_upon_replacement if { + not compat_login.allow with input.user as user + with input.session_counts as {"total": 65} + with input.login as {"type": "m.login.password"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} + + not compat_login.allow with input.user as user + with input.session_counts as {"total": 65} + with input.login as {"type": "m.login.sso"} + with input.session_replaced as false + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} +} diff --git a/policies/schema/compat_login_input.json b/policies/schema/compat_login_input.json new file mode 100644 index 000000000..ffb182de4 --- /dev/null +++ b/policies/schema/compat_login_input.json @@ -0,0 +1,144 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CompatLoginInput", + "description": "Input for the compatibility login policy.", + "type": "object", + "properties": { + "user": { + "type": "object", + "additionalProperties": true + }, + "session_counts": { + "description": "How many sessions the user has.", + "allOf": [ + { + "$ref": "#/definitions/SessionCounts" + } + ] + }, + "session_replaced": { + "description": "Whether a session will be replaced by this login", + "type": "boolean" + }, + "login": { + "description": "What type of login is being performed.\n This also determines whether the login is interactive.", + "allOf": [ + { + "$ref": "#/definitions/CompatLogin" + } + ] + }, + "requester": { + "$ref": "#/definitions/Requester" + } + }, + "required": [ + "user", + "session_counts", + "session_replaced", + "login", + "requester" + ], + "definitions": { + "SessionCounts": { + "description": "Information about how many sessions the user has", + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "oauth2": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "compat": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "personal": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "total", + "oauth2", + "compat", + "personal" + ] + }, + "CompatLogin": { + "oneOf": [ + { + "description": "Used as the interactive part of SSO login.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "m.login.sso" + }, + "redirect_uri": { + "type": "string" + } + }, + "required": [ + "type", + "redirect_uri" + ] + }, + { + "description": "Used as the final (non-interactive) stage of SSO login.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "m.login.token" + } + }, + "required": [ + "type" + ] + }, + { + "description": "Non-interactive password-over-the-API login.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "m.login.password" + } + }, + "required": [ + "type" + ] + } + ] + }, + "Requester": { + "description": "Identity of the requester", + "type": "object", + "properties": { + "ip_address": { + "description": "IP address of the entity making the request", + "type": [ + "string", + "null" + ], + "format": "ip" + }, + "user_agent": { + "description": "User agent of the entity making the request", + "type": [ + "string", + "null" + ] + } + } + } + } +} \ No newline at end of file diff --git a/templates/components/logout.html b/templates/components/logout.html index de4ee17dc..5db056120 100644 --- a/templates/components/logout.html +++ b/templates/components/logout.html @@ -6,16 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. -#} -{% macro button(text, csrf_token, as_link=false, post_logout_action={}) %} -
+{% macro button(csrf_token, text="", as_link=false, post_logout_action={}) %} + {% for key, value in post_logout_action|items %} {% endfor %} - {% if as_link %} - + {% if caller is defined %} + {{ caller() }} + {% elif as_link %} + {% else %} - + {% endif %}
{% endmacro %} diff --git a/templates/components/scope.html b/templates/components/scope.html index 96b5119cb..e6c7b6261 100644 --- a/templates/components/scope.html +++ b/templates/components/scope.html @@ -6,9 +6,38 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. -#} +{# Macro to remove 'safe' scope from a scope list. Usage: + + {% call(scopes) scope.unsafe_scopes(scopes=["openid", "urn:matrix:client:api:*", "urn:synapse:admin:*", "urn:mas:admin"]) %} + `scopes` only has unsafe scopes: ["urn:synapse:admin:*", "urn:mas:admin"] + +
    + {% for scope in scopes %} +
  • {{ scope }}
  • + {% endfor %} +
+ {% endcall %} +#} +{% macro unsafe_scopes(scopes) -%} + {% set ns = namespace(unsafe_scopes=[]) %} + {% set safe_scope_prefixes = ["openid", "urn:matrix:client:api:", "urn:matrix:org.matrix.msc2967.client:api:", "urn:matrix:client:device:", "urn:matrix:org.matrix.msc2967.client:device:"] %} + {% for scope in scopes %} + {% set ns.is_safe = False %} + {% for safe_scope_prefix in safe_scope_prefixes %} + {% if scope.startswith(safe_scope_prefix) %} + {% set ns.is_safe = True %} + {% endif %} + {% endfor %} + {% if not ns.is_safe %} + {% set ns.unsafe_scopes = ns.unsafe_scopes + [scope] %} + {% endif %} + {% endfor %} + {{ caller(ns.unsafe_scopes) }} +{%- endmacro %} + {% macro list(scopes) %}
    - {% for scope in (scopes | split(" ")) %} + {% for scope in scopes %} {% if scope == "openid" %}
  • {{ icon.user_profile() }}

    {{ _("mas.scope.view_profile") }}

  • {% elif scope == "urn:mas:graphql:*" %} @@ -18,9 +47,9 @@ Please see LICENSE files in the repository root for full details.
  • {{ icon.chat() }}

    {{ _("mas.scope.view_messages") }}

  • {{ icon.send() }}

    {{ _("mas.scope.send_messages") }}

  • {% elif scope == "urn:synapse:admin:*" %} -
  • {{ icon.room() }}

    {{ _("mas.scope.synapse_admin") }}

  • +
  • {{ icon.room() }}

    {{ _("mas.scope.synapse_admin", scope=scope) }}

  • {% elif scope == "urn:mas:admin" %} -
  • {{ icon.admin() }}

    {{ _("mas.scope.mas_admin") }}

  • +
  • {{ icon.admin() }}

    {{ _("mas.scope.mas_admin", scope=scope) }}

  • {% elif scope is startingwith("urn:matrix:client:device:") or scope is startingwith("urn:matrix:org.matrix.msc2967.client:device:") %} {# We hide this scope #} {% else %} diff --git a/templates/pages/compat_login_policy_violation.html b/templates/pages/compat_login_policy_violation.html new file mode 100644 index 000000000..80bb120c2 --- /dev/null +++ b/templates/pages/compat_login_policy_violation.html @@ -0,0 +1,31 @@ +{# +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +-#} + +{% extends "base.html" %} + +{% block content %} +
    +
    + {{ icon.error_solid() }} +
    + +
    +

    {{ _("mas.policy_violation.heading") }}

    +

    {{ _("mas.policy_violation.description") }}

    +
    +
    + +
    +
    +

    + {{ _("mas.policy_violation.logged_as", username=current_session.user.username) }} +

    + + {{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=True) }} +
    +
    +{% endblock content %} diff --git a/templates/pages/consent.html b/templates/pages/consent.html index 0eac9dc8b..c66e9c8a6 100644 --- a/templates/pages/consent.html +++ b/templates/pages/consent.html @@ -12,6 +12,7 @@ Please see LICENSE files in the repository root for full details. {% block content %} {% set client_name = client.client_name or client.client_id %} +
    {% if client.logo_uri %} @@ -22,33 +23,42 @@ Please see LICENSE files in the repository root for full details. {% endif %}
    -

    {{ _("mas.consent.heading") }}

    -

    - {{ _("mas.consent.client_wants_access", client_name=client_name, redirect_uri=(grant.redirect_uri | simplify_url)) }} - {{ _("mas.consent.this_will_allow", client_name=client_name) }} +

    + {{ _('mas.consent.continue_to', client_name=client_name) }} +

    +
    - - -
    - {{ _("mas.consent.make_sure_you_trust", client_name=client_name) }} - {{ _("mas.consent.you_may_be_sharing") }} - {% if client.policy_uri or client.tos_uri %} - Find out how {{ client_name }} will handle your data by reviewing its - {% if client.policy_uri %} - privacy policy{% if not client.tos_uri %}.{% endif %} - {% endif %} - {% if client.policy_uri and client.tos_uri%} - and - {% endif %} - {% if client.tos_uri %} - terms of service. - {% endif %} + {% call(scopes) scope.unsafe_scopes(scopes=grant.scope.split(" ")) %} + {% if scopes is not empty %} +
    +

    + {{ _('mas.consent.scope_list_preface', client_name=client_name) }} +

    + +
    {% endif %} + {% endcall %} + + {% set initial -%} + {%- if matrix_user.display_name -%} + {{- matrix_user.display_name[0] | upper -}} + {%- else -%} + {{- matrix_user.mxid[1] | upper -}} + {%- endif -%} + {%- endset %} + +
    +
    {{ initial }}
    +
    +
    {{ matrix_user.display_name or current_session.user.username }}
    +
    {{ matrix_user.mxid }}
    +
    @@ -57,13 +67,11 @@ Please see LICENSE files in the repository root for full details. {{ button.button(text=_("action.continue")) }} -
    -

    - {{ _("mas.not_you", username=current_session.user.username) }} -

    - - {{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }} -
    + {% call logout.button(csrf_token=csrf_token, post_logout_action=action) %} + + {% endcall %} {{ back_to_client.link( text=_("action.cancel"), diff --git a/templates/pages/device_consent.html b/templates/pages/device_consent.html index 548e14e31..23d56835d 100644 --- a/templates/pages/device_consent.html +++ b/templates/pages/device_consent.html @@ -25,9 +25,15 @@ Please see LICENSE files in the repository root for full details. {% endif %}
    -

    {{ _("mas.consent.heading") }}

    +

    + {{ _('mas.consent.continue_to', client_name=client_name) }} +

    -
    + + +
    {% if user_agent.device_type == "mobile" %} @@ -88,33 +94,36 @@ Please see LICENSE files in the repository root for full details.
    - -

    - {{ _("mas.device_consent.another_device_access") }} - {{ _("mas.consent.this_will_allow", client_name=client_name) }} -

    - - -
    - {{ _("mas.consent.make_sure_you_trust", client_name=client_name) }} - {{ _("mas.consent.you_may_be_sharing") }} - {% if client.policy_uri or client.tos_uri %} - Find out how {{ client_name }} will handle your data by reviewing its - {% if client.policy_uri %} - privacy policy{% if not client.tos_uri %}.{% endif %} - {% endif %} - {% if client.policy_uri and client.tos_uri%} - and - {% endif %} - {% if client.tos_uri %} - terms of service. - {% endif %} + {% call(scopes) scope.unsafe_scopes(scopes=grant.scope.split(" ")) %} + {% if scopes is not empty %} +
    +

    + {{ _('mas.consent.scope_list_preface', client_name=client_name) }} +

    + +
    {% endif %} + {% endcall %} + + {% set initial -%} + {%- if matrix_user.display_name -%} + {{- matrix_user.display_name[0] | upper -}} + {%- else -%} + {{- matrix_user.mxid[1] | upper -}} + {%- endif -%} + {%- endset %} + +
    +
    {{ initial }}
    +
    +
    {{ matrix_user.display_name or current_session.user.username }}
    +
    {{ matrix_user.mxid }}
    +
    @@ -123,18 +132,20 @@ Please see LICENSE files in the repository root for full details. - + {% endcall %} + +
    + +
    - -
    -

    - {{ _("mas.not_you", username=current_session.user.username) }} -

    - - {{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }} -
    {% elif grant.state == "rejected" %}
    diff --git a/templates/pages/sso.html b/templates/pages/sso.html index cecd92baf..f6d9aba99 100644 --- a/templates/pages/sso.html +++ b/templates/pages/sso.html @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. -#} +{% set consent_page = true %} + {% extends "base.html" %} {% block content %} @@ -17,18 +19,29 @@ Please see LICENSE files in the repository root for full details.
    -

    Allow access to your account?

    -

    {{ client_name }} wants to access your account. This will allow {{ client_name }} to:

    +

    + {{ _('mas.consent.continue_to', client_name=client_name) }} +

    +
    - + {% set initial -%} + {%- if matrix_user.display_name -%} + {{- matrix_user.display_name[0] | upper -}} + {%- else -%} + {{- matrix_user.mxid[1] | upper -}} + {%- endif -%} + {%- endset %} -
    - Make sure that you trust {{ client_name }}. - You may be sharing sensitive information with this site or app. +
    +
    {{ initial }}
    +
    +
    {{ matrix_user.display_name or current_session.user.username }}
    +
    {{ matrix_user.mxid }}
    +
    @@ -37,12 +50,10 @@ Please see LICENSE files in the repository root for full details. {{ button.button(text=_("action.continue")) }} -
    -

    - {{ _("mas.not_you", username=current_session.user.username) }} -

    - - {{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }} -
    + {% call logout.button(csrf_token=csrf_token, post_logout_action=action) %} + + {% endcall %}
    {% endblock content %} diff --git a/translations/en.json b/translations/en.json index c935542f3..53ac7ad3d 100644 --- a/translations/en.json +++ b/translations/en.json @@ -6,11 +6,11 @@ }, "cancel": "Cancel", "@cancel": { - "context": "pages/consent.html:69:11-29, pages/device_consent.html:127:13-31, pages/policy_violation.html:44:13-31" + "context": "pages/consent.html:77:11-29, pages/device_consent.html:146:13-31, pages/policy_violation.html:44:13-31" }, "continue": "Continue", "@continue": { - "context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:124:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:77:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48" + "context": "form_post.html:25:28-48, pages/consent.html:67:28-48, pages/device_consent.html:133:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:77:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:50:28-48" }, "create_account": "Create Account", "@create_account": { @@ -22,7 +22,7 @@ }, "sign_out": "Sign out", "@sign_out": { - "context": "pages/account/logged_out.html:22:28-48, pages/consent.html:65:28-48, pages/device_consent.html:136:30-50, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/sso.html:45:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46" + "context": "pages/account/logged_out.html:22:28-48, pages/compat_login_policy_violation.html:28:28-48, pages/index.html:28:28-48, pages/policy_violation.html:38:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46" }, "skip": "Skip", "@skip": { @@ -165,8 +165,6 @@ "@current": { "description": "Field for the user's current password" }, - "description": "This will change the password on your account.", - "@description": {}, "heading": "Change my password", "@heading": { "description": "Heading on the change password page" @@ -189,43 +187,39 @@ } }, "consent": { - "client_wants_access": "%(client_name)s at %(redirect_uri)s wants to access your account.", - "@client_wants_access": { - "context": "pages/consent.html:27:11-122" + "continue_to": "Continue to %(client_name)s?", + "@continue_to": { + "context": "pages/consent.html:27:11-64, pages/device_consent.html:29:13-66, pages/sso.html:23:11-64" }, - "heading": "Allow access to your account?", - "@heading": { - "context": "pages/consent.html:25:27-51, pages/device_consent.html:28:29-53" + "scope_list_preface": "By continuing, you allow %(client_name)s to:", + "@scope_list_preface": { + "context": "pages/consent.html:39:13-73, pages/device_consent.html:104:15-75" }, - "make_sure_you_trust": "Make sure that you trust %(client_name)s.", - "@make_sure_you_trust": { - "context": "pages/consent.html:38:81-142, pages/device_consent.html:104:83-144" + "this_will_setup": "This will set up %(client_name)s (%(client_uri)s) with your %(server_name)s account.", + "@this_will_setup": { + "context": "pages/consent.html:30:11-173" }, - "this_will_allow": "This will allow %(client_name)s to:", - "@this_will_allow": { - "context": "pages/consent.html:28:11-68, pages/device_consent.html:94:13-70" - }, - "you_may_be_sharing": "You may be sharing sensitive information with this site or app.", - "@you_may_be_sharing": { - "context": "pages/consent.html:39:7-42, pages/device_consent.html:105:9-44" + "use_another_account": "Use another account", + "@use_another_account": { + "context": "pages/consent.html:72:11-47, pages/device_consent.html:139:13-49, pages/sso.html:55:11-47" } }, "device_card": { "access_requested": "Access requested", "@access_requested": { - "context": "pages/device_consent.html:82:34-71" + "context": "pages/device_consent.html:88:34-71" }, "device_code": "Code", "@device_code": { - "context": "pages/device_consent.html:86:34-66" + "context": "pages/device_consent.html:92:34-66" }, "generic_device": "Device", "@generic_device": { - "context": "pages/device_consent.html:70:22-57" + "context": "pages/device_consent.html:76:22-57" }, "ip_address": "IP address", "@ip_address": { - "context": "pages/device_consent.html:77:36-67" + "context": "pages/device_consent.html:83:36-67" } }, "device_code_link": { @@ -239,29 +233,29 @@ } }, "device_consent": { - "another_device_access": "Another device wants to access your account.", - "@another_device_access": { - "context": "pages/device_consent.html:93:13-58" - }, "denied": { "description": "You denied access to %(client_name)s. You can close this window.", "@description": { - "context": "pages/device_consent.html:147:27-94" + "context": "pages/device_consent.html:158:27-94" }, "heading": "Access denied", "@heading": { - "context": "pages/device_consent.html:146:29-67" + "context": "pages/device_consent.html:157:29-67" } }, "granted": { "description": "You granted access to %(client_name)s. You can close this window.", "@description": { - "context": "pages/device_consent.html:158:27-95" + "context": "pages/device_consent.html:169:27-95" }, "heading": "Access granted", "@heading": { - "context": "pages/device_consent.html:157:29-68" + "context": "pages/device_consent.html:168:29-68" } + }, + "this_will_setup": "Another device wants to set up %(client_name)s (%(client_uri)s) with your %(server_name)s account. Make sure you recognise that device.", + "@this_will_setup": { + "context": "pages/device_consent.html:33:13-166" } }, "device_display_name": { @@ -416,6 +410,12 @@ "context": "components/field.html:32:11-45" } }, + "legacy_consent": { + "this_will_setup": "This will set up %(client_name)s with your %(server_name)s account.", + "@this_will_setup": { + "context": "pages/sso.html:26:11-109" + } + }, "login": { "call_to_register": "Don't have an account yet?", "@call_to_register": { @@ -485,7 +485,6 @@ }, "not_you": "Not %(username)s?", "@not_you": { - "context": "pages/consent.html:62:11-67, pages/device_consent.html:133:13-69, pages/sso.html:42:11-67", "description": "Suggestions for the user to log in as a different user" }, "or_separator": "Or", @@ -496,17 +495,17 @@ "policy_violation": { "description": "This might be because of the client which authored the request, the currently logged in user, or the request itself.", "@description": { - "context": "pages/policy_violation.html:19:25-62", + "context": "pages/compat_login_policy_violation.html:18:25-62, pages/policy_violation.html:19:25-62", "description": "Displayed when an authorization request is denied by the policy" }, "heading": "The authorization request was denied by the policy enforced by this service", "@heading": { - "context": "pages/policy_violation.html:18:27-60", + "context": "pages/compat_login_policy_violation.html:17:27-60, pages/policy_violation.html:18:27-60", "description": "Displayed when an authorization request is denied by the policy" }, "logged_as": "Logged as %(username)s", "@logged_as": { - "context": "pages/policy_violation.html:35:11-86" + "context": "pages/compat_login_policy_violation.html:25:11-86, pages/policy_violation.html:35:11-86" } }, "recovery": { @@ -656,36 +655,36 @@ "scope": { "edit_profile": "Edit your profile and contact details", "@edit_profile": { - "context": "components/scope.html:15:35-62", + "context": "components/scope.html:44:35-62", "description": "Displayed when the 'urn:mas:graphql:*' scope is requested" }, "manage_sessions": "Manage your devices and sessions", "@manage_sessions": { - "context": "components/scope.html:16:39-69", + "context": "components/scope.html:45:39-69", "description": "Displayed when the 'urn:mas:graphql:*' scope is requested" }, - "mas_admin": "Administer any user on the matrix-authentication-service", + "mas_admin": "Manage users (urn:mas:admin)", "@mas_admin": { - "context": "components/scope.html:23:54-78", + "context": "components/scope.html:52:54-91", "description": "Displayed when the 'urn:mas:admin' scope is requested" }, "send_messages": "Send new messages on your behalf", "@send_messages": { - "context": "components/scope.html:19:35-63" + "context": "components/scope.html:48:35-63" }, - "synapse_admin": "Administer the Synapse homeserver", + "synapse_admin": "Administer the server (urn:synapse:admin:*)", "@synapse_admin": { - "context": "components/scope.html:21:53-81", + "context": "components/scope.html:50:53-94", "description": "Displayed when the 'urn:synapse:admin:*' scope is requested" }, "view_messages": "View your existing messages and data", "@view_messages": { - "context": "components/scope.html:18:35-63", + "context": "components/scope.html:47:35-63", "description": "Displayed when the 'urn:matrix:client:api:*' scope is requested" }, "view_profile": "See your profile info and contact details", "@view_profile": { - "context": "components/scope.html:13:43-70", + "context": "components/scope.html:42:43-70", "description": "Displayed when the 'openid' scope is requested" } }, diff --git a/translations/et.json b/translations/et.json index 50f3550e7..66f957dac 100644 --- a/translations/et.json +++ b/translations/et.json @@ -74,9 +74,13 @@ }, "consent": { "client_wants_access": "%(client_name)s aadressil %(redirect_uri)s soovib ligipääsu sinu kasutajakontole.", + "continue_to": "Kas jätkad kliendis %(client_name)s?", "heading": "Kas lubad ligipääsu sinu kasutajakontole?", "make_sure_you_trust": "Palun kontrolli, et %(client_name)s on sinu jaoks usaldusväärne teenus.", + "scope_list_preface": "Jätkates lubad sa %(client_name)s kliendil:", "this_will_allow": "Sellega %(client_name)s saab õigused:", + "this_will_setup": "Sellega seadistad %(client_name)s (%(client_uri)s) kliendi kasutama sinu %(server_name)s kontot.", + "use_another_account": "Kasuta teist kontot", "you_may_be_sharing": "Sa tõenäoliselt jagad privaatset teavet selle veebisaidi või rakendusega." }, "device_card": { @@ -98,7 +102,8 @@ "granted": { "description": "Sa lubasid seadmele %(client_name)s ligipääsu. Sa võid nüüd selle akna sulgeda.", "heading": "Ligipääs on lubatud" - } + }, + "this_will_setup": "Üks teine seade tahab seadistada %(client_name)s (%(client_uri)s) klienti kasutama sinu %(server_name)s kontot. Palun kontrolli, et see on õige ja sinule vajalik seade." }, "device_display_name": { "client_on_device": "%(client_name)s seadmes %(device_name)s", @@ -145,6 +150,9 @@ "username_too_long": "Kasutajanimi on liiga pikk", "username_too_short": "Kasutajanimi on liiga lühike" }, + "legacy_consent": { + "this_will_setup": "Sellega seadistad %(client_name)s kliendi kasutama oma %(server_name)s kontot." + }, "login": { "call_to_register": "Sul veel pole kasutajakontot?", "continue_with_provider": "Jätka teenusepakkujaga %(provider)s", @@ -226,9 +234,9 @@ "scope": { "edit_profile": "Muuta sinu kasutajaprofiili ning kontaktandmeid", "manage_sessions": "Hallata sinu seadmeid ja sessioone", - "mas_admin": "Hallata iga kasutajat teenuses matrix-authentication-service", + "mas_admin": "Hallata kasutajaid (urn:mas:admin)", "send_messages": "Saata sõnumeid sinu nimel", - "synapse_admin": "Hallata seda Synapse koduserverit", + "synapse_admin": "Hallata seda Synapse koduserverit (urn:synapse:admin:*)", "view_messages": "Vaadata sinu sõnumeid ja andmeid", "view_profile": "Vaadata sinu profiili teavet ja kontaktadmeid" }, diff --git a/translations/fr.json b/translations/fr.json index c2cef20e0..e084bb929 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -74,9 +74,13 @@ }, "consent": { "client_wants_access": "%(client_name)s à l'adresse %(redirect_uri)s souhaite accéder à votre compte.", + "continue_to": "Continuer vers %(client_name)s?", "heading": "Autoriser l'accès à votre compte ?", "make_sure_you_trust": "Assurez-vous de faire confiance %(client_name)s.", + "scope_list_preface": "En continuant, vous autorisez %(client_name)s à :", "this_will_allow": "Cela va permettre à %(client_name)s de :", + "this_will_setup": "Continuer connectera %(client_name)s (%(client_uri)s) avec votre compte %(server_name)s.", + "use_another_account": "Utiliser un autre compte", "you_may_be_sharing": "Vous partagez peut-être des informations sensibles avec ce site ou cette application." }, "device_card": { @@ -98,7 +102,8 @@ "granted": { "description": "Vous avez accordé l'accès à %(client_name)s. Vous pouvez fermer cette fenêtre.", "heading": "Accès accordé" - } + }, + "this_will_setup": "Un autre appareil souhaite connecter %(client_name)s (%(client_uri)s) avec votre compte %(server_name)s. Assurez-vous de reconnaître cet appareil." }, "device_display_name": { "client_on_device": "%(client_name)s sur %(device_name)s", @@ -145,6 +150,9 @@ "username_too_long": "Le nom d'utilisateur est trop long", "username_too_short": "Le nom d'utilisateur est trop court" }, + "legacy_consent": { + "this_will_setup": "Continuer connectera %(client_name)s avec votre compte %(server_name)s." + }, "login": { "call_to_register": "Vous n’avez pas encore de compte ?", "continue_with_provider": "Poursuivre avec %(provider)s", @@ -226,9 +234,9 @@ "scope": { "edit_profile": "Modifier votre profil et vos coordonnées", "manage_sessions": "Gérer vos appareils et vos sessions", - "mas_admin": "Administrer n'importe quel utilisateur dans matrix-authentication-service", + "mas_admin": "Administrer les utilisateurs (urn:mas:admin)", "send_messages": "Envoyez de nouveaux messages en votre nom", - "synapse_admin": "Administrer le serveur d’accueil Synapse", + "synapse_admin": "Administrer le serveur (urn:synapse:admin:*)", "view_messages": "Afficher vos messages et données existants", "view_profile": "Voir les informations de votre profil et vos coordonnées" }, diff --git a/translations/uk.json b/translations/uk.json index fcdf0ec6d..941d1ee27 100644 --- a/translations/uk.json +++ b/translations/uk.json @@ -74,9 +74,13 @@ }, "consent": { "client_wants_access": "%(client_name)s за %(redirect_uri)s хоче отримати доступ до вашого облікового запису.", + "continue_to": "Продовжити в %(client_name)s?", "heading": "Дозволити доступ до свого облікового запису?", "make_sure_you_trust": "Переконайтеся, що ви довіряєте %(client_name)s.", + "scope_list_preface": "Продовжуючи, ви дозволяєте %(client_name)s:", "this_will_allow": "Це дозволить %(client_name)s:", + "this_will_setup": "Це налаштує %(client_name)s (%(client_uri)s) з вашим обліковим записом %(server_name)s.", + "use_another_account": "Використати інший обліковий запис", "you_may_be_sharing": "Можливо, ви ділитеся конфіденційною інформацією з цим сайтом або застосунком." }, "device_card": { @@ -98,7 +102,8 @@ "granted": { "description": "Ви надали доступ до %(client_name)s. Ви можете закрити це вікно.", "heading": "Доступ надано" - } + }, + "this_will_setup": "Інший пристрій хоче налаштувати %(client_name)s (%(client_uri)s) з вашим обліковим записом %(server_name)s. Переконайтеся, що ви розпізнаєте цей пристрій." }, "device_display_name": { "client_on_device": "%(client_name)s на %(device_name)s", @@ -145,6 +150,9 @@ "username_too_long": "Ім'я користувача задовге", "username_too_short": "Ім'я користувача закоротке" }, + "legacy_consent": { + "this_will_setup": "Це налаштує %(client_name)s з вашим обліковим записом %(server_name)s." + }, "login": { "call_to_register": "У вас ще немає облікового запису?", "continue_with_provider": "Продовжити з %(provider)s", @@ -210,6 +218,7 @@ "register": { "call_to_login": "Вже маєте обліковий запис?", "continue_with_email": "Продовжити за допомогою е-пошти", + "continue_with_password": "Продовжити з паролем", "create_account": { "description": "Виберіть ім'я користувача, щоб продовжити.", "heading": "Створити обліковий запис" @@ -225,9 +234,9 @@ "scope": { "edit_profile": "Редагування профілю та контактних даних", "manage_sessions": "Керування пристроями та сеансами", - "mas_admin": "Адміністрування будь-якого користувача на matrix-authentication-service", + "mas_admin": "Керування користувачами (urn:mas:admin)", "send_messages": "Надсилати нові повідомлення від вашого імені", - "synapse_admin": "Адміністрування домашнього сервера Synapse", + "synapse_admin": "Адмініструвати сервер (urn:synapse:admin:*)", "view_messages": "Перегляд наявних повідомлень і даних", "view_profile": "Перегляд інформації профілю та контактних даних" },