diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..222697181 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @element-hq/mas-maintainers diff --git a/.github/actions/build-frontend/action.yml b/.github/actions/build-frontend/action.yml index 417f1f9ae..08e2cf6f0 100644 --- a/.github/actions/build-frontend/action.yml +++ b/.github/actions/build-frontend/action.yml @@ -10,9 +10,9 @@ runs: using: composite steps: - name: Install Node - uses: actions/setup-node@v4.2.0 + uses: actions/setup-node@v6.0.0 with: - node-version: "22" + node-version: "24" - name: Install dependencies run: npm ci diff --git a/.github/actions/build-policies/action.yml b/.github/actions/build-policies/action.yml index 0eba08e6e..e1dc28547 100644 --- a/.github/actions/build-policies/action.yml +++ b/.github/actions/build-policies/action.yml @@ -12,7 +12,8 @@ runs: - name: Install Open Policy Agent uses: open-policy-agent/setup-opa@v2.2.0 with: - version: 1.1.0 + # Keep in sync with the Dockerfile and policies/Makefile + version: 1.8.0 - name: Build the policies run: make diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f17962b3c..548e53eca 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -84,7 +84,7 @@ jobs: chmod -R u=rwX,go=rX assets-dist/ - name: Upload assets - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v5.0.0 with: name: assets path: assets-dist @@ -143,7 +143,7 @@ jobs: -p mas-cli - name: Upload binary artifact - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v5.0.0 with: name: binary-${{ matrix.target }} path: target/${{ matrix.target }}/release/mas-cli @@ -162,19 +162,19 @@ jobs: steps: - name: Download assets - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: assets path: assets-dist - name: Download binary x86_64 - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: binary-x86_64-unknown-linux-gnu path: binary-x86_64 - name: Download binary aarch64 - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: binary-aarch64-unknown-linux-gnu path: binary-aarch64 @@ -192,13 +192,13 @@ jobs: done - name: Upload aarch64 archive - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v5.0.0 with: name: mas-cli-aarch64-linux path: mas-cli-aarch64-linux.tar.gz - name: Upload x86_64 archive - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v5.0.0 with: name: mas-cli-x86_64-linux path: mas-cli-x86_64-linux.tar.gz @@ -226,7 +226,7 @@ jobs: steps: - name: Docker meta id: meta - uses: docker/metadata-action@v5.8.0 + uses: docker/metadata-action@v5.9.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.8.0 + uses: docker/metadata-action@v5.9.0 with: images: "${{ env.IMAGE }}" bake-target: docker-metadata-action-debug @@ -258,7 +258,7 @@ jobs: type=sha - name: Setup Cosign - uses: sigstore/cosign-installer@v3.9.2 + uses: sigstore/cosign-installer@v4.0.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.11.1 @@ -268,7 +268,7 @@ jobs: mirrors = ["mirror.gcr.io"] - name: Login to GitHub Container Registry - uses: docker/login-action@v3.5.0 + uses: docker/login-action@v3.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -320,14 +320,14 @@ jobs: - build-image steps: - name: Download the artifacts from the previous job - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: mas-cli-* path: artifacts merge-multiple: true - name: Prepare a release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@v2.4.2 with: generate_release_notes: true body: | @@ -382,21 +382,21 @@ jobs: .github/scripts - name: Download the artifacts from the previous job - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: mas-cli-* path: artifacts merge-multiple: true - name: Update unstable git tag - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | const script = require('./.github/scripts/update-unstable-tag.cjs'); await script({ core, github, context }); - name: Update unstable release - uses: softprops/action-gh-release@v2.3.2 + uses: softprops/action-gh-release@v2.4.2 with: name: "Unstable build" tag_name: unstable @@ -460,7 +460,7 @@ jobs: .github/scripts - name: Remove label and comment - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 env: BUILD_IMAGE_MANIFEST: ${{ needs.build-image.outputs.metadata }} with: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 50ef76b8c..ed119c2e3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,8 @@ jobs: - name: Setup Regal uses: StyraInc/setup-regal@v1 with: - version: 0.29.2 + # Keep in sync with policies/Makefile + version: 0.36.1 - name: Lint policies working-directory: ./policies @@ -63,9 +64,9 @@ jobs: uses: actions/checkout@v5 - name: Install Node - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.0.0 with: - node-version: 22 + node-version: 24 - name: Install Node dependencies working-directory: ./frontend @@ -87,9 +88,9 @@ jobs: uses: actions/checkout@v5 - name: Install Node - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.0.0 with: - node-version: 22 + node-version: 24 - name: Install Node dependencies working-directory: ./frontend @@ -111,9 +112,9 @@ jobs: uses: actions/checkout@v5 - name: Install Node - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.0.0 with: - node-version: 20 + node-version: 24 - name: Install Node dependencies working-directory: ./frontend @@ -256,7 +257,7 @@ jobs: SQLX_OFFLINE: "1" - name: Upload archive to workflow - uses: actions/upload-artifact@v4.6.2 + uses: actions/upload-artifact@v5.0.0 with: name: nextest-archive path: nextest-archive.tar.zst @@ -304,7 +305,7 @@ jobs: - uses: ./.github/actions/build-policies - name: Download archive - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: nextest-archive diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 3ac22da5d..3046f45cc 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -38,7 +38,7 @@ jobs: run: make coverage - name: Upload to codecov.io - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: policies/coverage.json @@ -65,7 +65,7 @@ jobs: run: npm run coverage - name: Upload to codecov.io - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} directory: frontend/coverage/ @@ -132,7 +132,7 @@ jobs: grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov - name: Upload to codecov.io - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} files: target/coverage/*.lcov diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index a0ebd9361..84074d85e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -39,9 +39,9 @@ jobs: tool: mdbook - name: Install Node - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.0.0 with: - node-version: 22 + node-version: 24 - name: Build the documentation run: sh misc/build-docs.sh diff --git a/.github/workflows/merge-back.yaml b/.github/workflows/merge-back.yaml index 17b0e0804..d28d68c20 100644 --- a/.github/workflows/merge-back.yaml +++ b/.github/workflows/merge-back.yaml @@ -30,7 +30,7 @@ jobs: .github/scripts - name: Push branch and open a PR - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 env: SHA: ${{ inputs.sha }} with: diff --git a/.github/workflows/release-branch.yaml b/.github/workflows/release-branch.yaml index 1d52cd60b..e31fa2c82 100644 --- a/.github/workflows/release-branch.yaml +++ b/.github/workflows/release-branch.yaml @@ -64,9 +64,9 @@ jobs: uses: actions/checkout@v5 - name: Install Node - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.0.0 with: - node-version: 22 + node-version: 24 - name: Install Localazy CLI run: npm install -g @localazy/cli @@ -112,7 +112,7 @@ jobs: .github/scripts - name: Create a new release branch - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 env: BRANCH: release/v${{ needs.compute-version.outputs.short }} SHA: ${{ needs.tag.outputs.sha }} diff --git a/.github/workflows/release-bump.yaml b/.github/workflows/release-bump.yaml index 22b157a5e..46251a410 100644 --- a/.github/workflows/release-bump.yaml +++ b/.github/workflows/release-bump.yaml @@ -82,7 +82,7 @@ jobs: .github/scripts - name: Update the release branch - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 env: BRANCH: "${{ github.ref_name }}" SHA: ${{ needs.tag.outputs.sha }} diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index cf894e8af..ec3ea290d 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -46,7 +46,7 @@ jobs: run: cargo metadata --format-version 1 - name: Commit and tag using the GitHub API - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 id: commit env: VERSION: ${{ inputs.version }} @@ -58,7 +58,7 @@ jobs: return await script({ core, github, context }); - name: Update the refs - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 env: VERSION: ${{ inputs.version }} TAG_SHA: ${{ fromJSON(steps.commit.outputs.result).tag }} diff --git a/.github/workflows/translations-download.yaml b/.github/workflows/translations-download.yaml index 1832e2b9c..a022203fc 100644 --- a/.github/workflows/translations-download.yaml +++ b/.github/workflows/translations-download.yaml @@ -22,9 +22,9 @@ jobs: uses: actions/checkout@v5 - name: Install Node - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.0.0 with: - node-version: 22 + node-version: 24 - name: Install Localazy CLI run: npm install -g @localazy/cli diff --git a/.github/workflows/translations-upload.yaml b/.github/workflows/translations-upload.yaml index 3121617a4..063a228dd 100644 --- a/.github/workflows/translations-upload.yaml +++ b/.github/workflows/translations-upload.yaml @@ -21,9 +21,9 @@ jobs: uses: actions/checkout@v5 - name: Install Node - uses: actions/setup-node@v4.4.0 + uses: actions/setup-node@v6.0.0 with: - node-version: 22 + node-version: 24 - name: Install Localazy CLI run: npm install -g @localazy/cli diff --git a/Cargo.lock b/Cargo.lock index 29350974d..f3eb7e0ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "aide" -version = "0.14.2" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2477554ebf38aea815a9c4729100cfc32f766876c45b9c9c38ef221b9d1a703" +checksum = "6966317188cdfe54c58c0900a195d021294afb3ece9b7073d09e4018dbb1e3a2" dependencies = [ "aide-macros", "axum", @@ -95,12 +95,12 @@ dependencies = [ "bytes", "cfg-if", "http", - "indexmap 2.11.0", - "schemars 0.8.22", + "indexmap 2.11.4", + "schemars 0.9.0", "serde", "serde_json", "serde_qs", - "thiserror 2.0.16", + "thiserror 2.0.17", "tower-layer", "tower-service", "tracing", @@ -108,11 +108,12 @@ dependencies = [ [[package]] name = "aide-macros" -version = "0.8.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8e0d4af7cc08353807aaf80722125a229bf2d67be7fe0b89163c648db3d223" +checksum = "9f2a08f14808f3c46f3e3004b727bace64af44c3c5996d0480a14d3852b1b25a" dependencies = [ "darling", + "proc-macro2", "quote", "syn", ] @@ -123,12 +124,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -140,9 +135,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -170,35 +165,35 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" @@ -271,9 +266,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -316,7 +311,7 @@ dependencies = [ "futures-timer", "futures-util", "http", - "indexmap 2.11.0", + "indexmap 2.11.4", "mime", "multer", "num-traits", @@ -368,45 +363,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap 2.11.0", + "indexmap 2.11.4", "serde", "serde_json", ] [[package]] name = "async-io" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", - "rustix 1.0.8", + "rustix", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "async-lock" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel 2.5.0", "async-io", @@ -415,16 +410,16 @@ dependencies = [ "async-task", "blocking", "cfg-if", - "event-listener 5.4.0", + "event-listener 5.4.1", "futures-lite", - "rustix 1.0.8", + "rustix", ] [[package]] name = "async-signal" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", @@ -432,17 +427,17 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.0.8", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "async-std" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-channel 1.9.0", "async-global-executor", @@ -536,9 +531,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" dependencies = [ "aws-lc-sys", "zeroize", @@ -546,9 +541,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" dependencies = [ "bindgen", "cc", @@ -559,9 +554,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", "bytes", @@ -578,8 +573,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -593,9 +587,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -604,7 +598,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -613,14 +606,15 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ "axum", "axum-core", "bytes", "cookie", + "form_urlencoded", "futures-util", "headers", "http", @@ -629,10 +623,12 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "serde", - "tower", + "serde_core", + "serde_html_form", + "serde_path_to_error", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -648,9 +644,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -658,7 +654,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -694,25 +690,22 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.5" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn", - "which", ] [[package]] @@ -732,9 +725,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] @@ -800,9 +793,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -831,11 +824,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.11" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -858,10 +851,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -884,9 +878,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chacha20" @@ -914,17 +908,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -980,9 +973,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.46" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" dependencies = [ "clap_builder", "clap_derive", @@ -990,9 +983,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.46" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" dependencies = [ "anstream", "anstyle", @@ -1002,9 +995,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1033,7 +1026,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1173,36 +1166,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae7b60ec3fd7162427d3b3801520a1908bef7c035b52983cd3ca11b8e7deb51" +checksum = "c088d3406f0c0252efa7445adfd2d05736bfb5218838f64eaf79d567077aed14" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6511c200fed36452697b4b6b161eae57d917a2044e6333b1c1389ed63ccadeee" +checksum = "5c03f887a763abb9c1dc08f722aa82b69067fda623b6f0273050f45f8b1a6776" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7086a645aa58bae979312f64e3029ac760ac1b577f5cd2417844842a2ca07f" +checksum = "0206887a11a43f507fee320a218dc365980bfc42ec2696792079a9f8c9369e90" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5225b4dec45f3f3dbf383f12560fac5ce8d780f399893607e21406e12e77f491" +checksum = "ac0790c83cfdab95709c5d0105fd888221e3af9049a7d7ec376ec901ab4e4dba" dependencies = [ "serde", "serde_derive", @@ -1210,9 +1203,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "858fb3331e53492a95979378d6df5208dd1d0d315f19c052be8115f4efc888e0" +checksum = "9a98aed2d262eda69310e84bae8e053ee4f17dbdd3347b8d9156aa618ba2de0a" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1224,11 +1217,11 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "log", "pulley-interpreter", "regalloc2", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "smallvec", "target-lexicon", @@ -1237,36 +1230,37 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456715b9d5f12398f156d5081096e7b5d039f01b9ecc49790a011c8e43e65b5f" +checksum = "6906852826988563e9b0a9232ad951f53a47aa41ffd02f8ac852d3f41aae836a" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", "cranelift-srcgen", + "heck 0.5.0", "pulley-interpreter", ] [[package]] name = "cranelift-codegen-shared" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0306041099499833f167a0ddb707e1e54100f1a84eab5631bc3dad249708f482" +checksum = "3a50105aab667b5cc845f2be37c78475d7cc127cd8ec0a31f7b2b71d526099a7" [[package]] name = "cranelift-control" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1672945e1f9afc2297f49c92623f5eabc64398e2cb0d824f8f72a2db2df5af23" +checksum = "6adcc7aa7c0bc1727176a6f2d99c28a9e79a541ccd5ca911a0cb352da8befa36" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa3cd55eb5f3825b9ae5de1530887907360a6334caccdc124c52f6d75246c98a" +checksum = "981b56af777f9a34ea6dcce93255125776d391410c2a68b75bed5941b714fa15" dependencies = [ "cranelift-bitset", "serde", @@ -1275,9 +1269,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781f9905f8139b8de22987b66b522b416fe63eb76d823f0b3a8c02c8fd9500c7" +checksum = "dea982589684dfb71afecb9fc09555c3a266300a1162a60d7fa39d41a5705b1c" dependencies = [ "cranelift-codegen", "log", @@ -1287,15 +1281,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05337a2b02c3df00b4dd9a263a027a07b3dff49f61f7da3b5d195c21eaa633d" +checksum = "a0422686b22ed6a1f33cc40e3c43eb84b67155788568d1a5cac8439d3dca1783" [[package]] name = "cranelift-native" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eee7a496dd66380082c9c5b6f2d5fa149cec0ec383feec5caf079ca2b3671c2" +checksum = "56f697bbbe135c655ea1deb7af0bae4a5c4fae2c88fdfc0fa57b34ae58c91040" dependencies = [ "cranelift-codegen", "libc", @@ -1304,9 +1298,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.122.0" +version = "0.125.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b530783809a55cb68d070e0de60cfbb3db0dc94c8850dd5725411422bedcf6bb" +checksum = "718efe674f3df645462677e22a3128e890d88ba55821bb091083d257707be76c" [[package]] name = "crc" @@ -1411,14 +1405,14 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] @@ -1490,11 +1484,12 @@ dependencies = [ [[package]] name = "deadpool" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" dependencies = [ "deadpool-runtime", + "lazy_static", "num_cpus", "tokio", ] @@ -1528,9 +1523,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", "serde", @@ -1631,8 +1626,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb333721800c025e363e902b293040778f8ac79913db4f013abf1f1d7d382fd7" dependencies = [ "rust_decimal", - "thiserror 2.0.16", - "winnow 0.7.12", + "thiserror 2.0.17", + "winnow 0.7.13", ] [[package]] @@ -1736,12 +1731,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1763,9 +1758,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -1778,7 +1773,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "pin-project-lite", ] @@ -1831,6 +1826,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "fixed_decimal" version = "0.5.6" @@ -1941,9 +1942,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -2039,7 +2040,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -2054,20 +2055,20 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", - "indexmap 2.11.0", + "indexmap 2.11.4", "stable_deref_trait", ] [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gloo-timers" @@ -2092,7 +2093,7 @@ dependencies = [ "futures-sink", "futures-timer", "futures-util", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "nonzero_ext", "parking_lot", "portable-atomic", @@ -2115,9 +2116,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -2125,7 +2126,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.0", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -2150,9 +2151,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -2166,7 +2167,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -2252,7 +2253,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2348,9 +2349,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", @@ -2372,9 +2373,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2733,13 +2734,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "serde", + "serde_core", ] [[package]] @@ -2750,9 +2752,9 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "inherent" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" dependencies = [ "proc-macro2", "quote", @@ -2777,9 +2779,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.1" +version = "1.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console", "once_cell", @@ -2787,17 +2789,6 @@ dependencies = [ "similar", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2810,7 +2801,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" dependencies = [ - "schemars 0.8.22", "serde", ] @@ -2830,15 +2820,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -2887,9 +2868,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -2897,9 +2878,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -2977,12 +2958,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -2991,9 +2966,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" +checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" dependencies = [ "async-std", "async-trait", @@ -3022,9 +2997,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -3042,6 +3017,17 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -3054,15 +3040,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "listenfd" @@ -3099,9 +3079,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "value-bag", ] @@ -3117,7 +3097,7 @@ dependencies = [ [[package]] name = "mas-axum-utils" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "axum", @@ -3142,7 +3122,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "ulid", @@ -3151,7 +3131,7 @@ dependencies = [ [[package]] name = "mas-cli" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "axum", @@ -3192,12 +3172,11 @@ dependencies = [ "opentelemetry-http", "opentelemetry-jaeger-propagator", "opentelemetry-otlp", - "opentelemetry-prometheus", + "opentelemetry-prometheus-text-exporter", "opentelemetry-resource-detectors", "opentelemetry-semantic-conventions", "opentelemetry-stdout", "opentelemetry_sdk", - "prometheus", "rand 0.8.5", "rand_chacha 0.3.1", "reqwest", @@ -3225,7 +3204,7 @@ dependencies = [ [[package]] name = "mas-config" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "camino", @@ -3245,7 +3224,7 @@ dependencies = [ "rand_chacha 0.3.1", "rustls-pemfile", "rustls-pki-types", - "schemars 0.8.22", + "schemars 0.9.0", "serde", "serde_json", "serde_with", @@ -3257,7 +3236,7 @@ dependencies = [ [[package]] name = "mas-context" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "console", "opentelemetry", @@ -3273,7 +3252,7 @@ dependencies = [ [[package]] name = "mas-data-model" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "base64ct", "chrono", @@ -3288,7 +3267,7 @@ dependencies = [ "ruma-common", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "ulid", "url", "woothee", @@ -3296,18 +3275,18 @@ dependencies = [ [[package]] name = "mas-email" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "async-trait", "lettre", "mas-templates", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] [[package]] name = "mas-handlers" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "aide", "anyhow", @@ -3329,7 +3308,7 @@ dependencies = [ "hex", "hyper", "icu_normalizer", - "indexmap 2.11.0", + "indexmap 2.11.4", "insta", "lettre", "mas-axum-utils", @@ -3363,7 +3342,7 @@ dependencies = [ "rand_chacha 0.3.1", "reqwest", "rustls", - "schemars 0.8.22", + "schemars 0.9.0", "sentry", "serde", "serde_json", @@ -3371,7 +3350,7 @@ dependencies = [ "serde_with", "sha2", "sqlx", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-util", "tower", @@ -3387,7 +3366,7 @@ dependencies = [ [[package]] name = "mas-http" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "futures-util", "headers", @@ -3408,7 +3387,7 @@ dependencies = [ [[package]] name = "mas-i18n" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "camino", "icu_calendar", @@ -3424,13 +3403,13 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "writeable", ] [[package]] name = "mas-i18n-scan" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "camino", "clap", @@ -3444,15 +3423,15 @@ dependencies = [ [[package]] name = "mas-iana" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ - "schemars 0.8.22", + "schemars 0.9.0", "serde", ] [[package]] name = "mas-iana-codegen" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "async-trait", @@ -3460,6 +3439,7 @@ dependencies = [ "convert_case", "csv", "reqwest", + "rustls", "serde", "tokio", "tracing", @@ -3468,7 +3448,7 @@ dependencies = [ [[package]] name = "mas-jose" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "base64ct", "chrono", @@ -3485,20 +3465,20 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rsa", - "schemars 0.8.22", + "schemars 0.9.0", "sec1", "serde", "serde_json", "serde_with", "sha2", "signature", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", ] [[package]] name = "mas-keystore" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "aead", "base64ct", @@ -3521,12 +3501,12 @@ dependencies = [ "rsa", "sec1", "spki", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "mas-listener" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "bytes", @@ -3538,7 +3518,7 @@ dependencies = [ "pin-project-lite", "rustls-pemfile", "socket2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-rustls", "tokio-test", @@ -3551,7 +3531,7 @@ dependencies = [ [[package]] name = "mas-matrix" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "async-trait", @@ -3561,7 +3541,7 @@ dependencies = [ [[package]] name = "mas-matrix-synapse" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "async-trait", @@ -3570,7 +3550,7 @@ dependencies = [ "mas-matrix", "reqwest", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", "urlencoding", @@ -3578,7 +3558,7 @@ dependencies = [ [[package]] name = "mas-oidc-client" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "assert_matches", "async-trait", @@ -3605,7 +3585,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -3614,24 +3594,24 @@ dependencies = [ [[package]] name = "mas-policy" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "arc-swap", "mas-data-model", "oauth2-types", "opa-wasm", - "schemars 0.8.22", + "schemars 0.9.0", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] [[package]] name = "mas-router" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "axum", "serde", @@ -3642,16 +3622,16 @@ dependencies = [ [[package]] name = "mas-spa" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "camino", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "mas-storage" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "async-trait", "chrono", @@ -3664,7 +3644,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "tracing-opentelemetry", "ulid", @@ -3673,7 +3653,7 @@ dependencies = [ [[package]] name = "mas-storage-pg" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "async-trait", "chrono", @@ -3690,8 +3670,9 @@ dependencies = [ "sea-query", "sea-query-binder", "serde_json", + "sha2", "sqlx", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "ulid", "url", @@ -3700,7 +3681,7 @@ dependencies = [ [[package]] name = "mas-tasks" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "async-trait", @@ -3722,7 +3703,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -3732,7 +3713,7 @@ dependencies = [ [[package]] name = "mas-templates" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "arc-swap", @@ -3751,7 +3732,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "ulid", @@ -3762,7 +3743,7 @@ dependencies = [ [[package]] name = "mas-tower" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "http", "opentelemetry", @@ -3807,11 +3788,11 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memfd" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 0.38.44", + "rustix", ] [[package]] @@ -4032,12 +4013,12 @@ dependencies = [ [[package]] name = "oauth2-types" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "assert_matches", "base64ct", "chrono", - "indexmap 2.11.0", + "indexmap 2.11.4", "insta", "language-tags", "mas-iana", @@ -4046,19 +4027,19 @@ dependencies = [ "serde_json", "serde_with", "sha2", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", ] [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "crc32fast", - "hashbrown 0.15.4", - "indexmap 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.11.4", "memchr", ] @@ -4076,9 +4057,9 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opa-wasm" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd2bab45ae1b87f45b4ddea74902158543322dc49bf45d2f714c50bbf8cf44f" +checksum = "abd89491a7de9144566be8bca70853333dfd2859077432c65e45c8bfc00b6234" dependencies = [ "anyhow", "base64", @@ -4102,7 +4083,7 @@ dependencies = [ "sha1", "sha2", "sprintf", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "urlencoding", @@ -4124,23 +4105,23 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "opentelemetry" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" dependencies = [ "futures-core", "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] [[package]] name = "opentelemetry-http" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", @@ -4151,18 +4132,18 @@ dependencies = [ [[package]] name = "opentelemetry-jaeger-propagator" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090b8ec07bb2e304b529581aa1fe530d7861298c9ef549ebbf44a4a56472c539" +checksum = "ba3bbd907f151104a112f749f3b8387ef669b7264e0bb80546ea0700a3b307b7" dependencies = [ "opentelemetry", ] [[package]] name = "opentelemetry-otlp" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" dependencies = [ "http", "opentelemetry", @@ -4170,38 +4151,38 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] -name = "opentelemetry-prometheus" -version = "0.29.1" -source = "git+https://github.com/sandhose/opentelemetry-rust.git?branch=otel-prometheus-0.30#193906c7577b4f8ee642aa771191c7d80b14a297" +name = "opentelemetry-prometheus-text-exporter" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897906366b17a89bec845f6051e0c3474049402a09a0711eea180941293bd013" dependencies = [ - "once_cell", "opentelemetry", "opentelemetry_sdk", - "prometheus", - "tracing", + "smartstring", ] [[package]] name = "opentelemetry-proto" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry-resource-detectors" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a44e076f07fa3d76e741991f4f7d3ecbac0eed8521ced491fbdf8db77d024cf" +checksum = "e82845106cf72d47c141cee7f0d95e0650d8f28c6222a1f1ae727a8883899c19" dependencies = [ "opentelemetry", "opentelemetry-semantic-conventions", @@ -4210,15 +4191,15 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" +checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" [[package]] name = "opentelemetry-stdout" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447191061af41c3943e082ea359ab8b64ff27d6d34d30d327df309ddef1eef6f" +checksum = "bc8887887e169414f637b18751487cce4e095be787d23fad13c454e2fb1b3811" dependencies = [ "chrono", "opentelemetry", @@ -4227,9 +4208,9 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" dependencies = [ "futures-channel", "futures-executor", @@ -4237,8 +4218,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.2", - "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", ] @@ -4387,20 +4367,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -4408,9 +4387,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", @@ -4421,9 +4400,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2", @@ -4536,12 +4515,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64", - "indexmap 2.11.0", + "indexmap 2.11.4", "quick-xml", "serde", "time", @@ -4549,16 +4528,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix", + "windows-sys 0.61.0", ] [[package]] @@ -4619,9 +4598,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -4647,9 +4626,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -4667,26 +4646,11 @@ dependencies = [ "yansi", ] -[[package]] -name = "prometheus" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" -dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "memchr", - "parking_lot", - "protobuf", - "thiserror 2.0.16", -] - [[package]] name = "prost" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -4694,9 +4658,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools 0.14.0", @@ -4705,31 +4669,11 @@ dependencies = [ "syn", ] -[[package]] -name = "protobuf" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" -dependencies = [ - "once_cell", - "protobuf-support", - "thiserror 1.0.69", -] - -[[package]] -name = "protobuf-support" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "psl" -version = "2.1.136" +version = "2.1.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4d5ec1bed313b61a9d525e8549493538aea497056a5484f5398b1a05bb8261" +checksum = "9031bc0c03e389af72e52f11bc07869fbcc357f1b0774f9e2b3bce085a393416" dependencies = [ "psl-types", ] @@ -4751,9 +4695,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c4319786b16c1a6a38ee04788d32c669b61ba4b69da2162c868c18be99c1b" +checksum = "beafc309a2d35e16cc390644d88d14dfa45e45e15075ec6a9e37f6dfb43e926f" dependencies = [ "cranelift-bitset", "log", @@ -4763,9 +4707,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938543690519c20c3a480d20a8efcc8e69abeb44093ab1df4e7c1f81f26c677a" +checksum = "1885fbb6c07454cfc8725a18a1da3cfc328ee8c53fb8d0671ea313edc8567947" dependencies = [ "proc-macro2", "quote", @@ -4789,9 +4733,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.0" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] @@ -4878,18 +4822,18 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.5.0" +version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ "bitflags", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -4897,9 +4841,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -4936,23 +4880,23 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.12.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +checksum = "4e249c660440317032a71ddac302f25f1d5dff387667bcc3978d1f77aa31ac34" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "log", - "rustc-hash 2.1.1", + "rustc-hash", "smallvec", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -4962,9 +4906,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -4973,15 +4917,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -5064,15 +5008,15 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387e1898e868d32ff7b205e7db327361d5dcf635c00a8ae5865068607595a9cf" +checksum = "ac7f59b9f7639667d0d6ae3ae242c8912e9ed061cea1fbaf72710a402e83b53e" dependencies = [ "as_variant", "base64", "bytes", "form_urlencoded", - "indexmap 2.11.0", + "indexmap 2.11.4", "js_int", "percent-encoding", "regex", @@ -5081,29 +5025,30 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "tracing", "url", "web-time", "wildmatch", + "zeroize", ] [[package]] name = "ruma-identifiers-validation" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad674b5e5368c53a2c90fde7dac7e30747004aaf7b1827b72874a25fc06d4d8" +checksum = "14a7b93ac1e571c585f8fa5cef09c07bb8a15529775fd56b9a3eac4f9233dff2" dependencies = [ "js_int", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "ruma-macros" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff13fbd6045a7278533390826de316d6116d8582ed828352661337b0c422e1c" +checksum = "0c9911c7188517f28505d2d513339511d00e0f50cec5c2dde820cd0ec7e6a833" dependencies = [ "cfg-if", "proc-macro-crate", @@ -5117,9 +5062,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.37.2" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" +checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" dependencies = [ "arrayvec", "num-traits", @@ -5131,12 +5076,6 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -5154,35 +5093,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys", + "windows-sys 0.61.0", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", @@ -5216,9 +5142,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "zeroize", ] @@ -5252,9 +5178,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "aws-lc-rs", "ring", @@ -5264,9 +5190,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -5294,27 +5220,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "chrono", - "dyn-clone", - "indexmap 1.9.3", - "indexmap 2.11.0", - "schemars_derive", - "serde", - "serde_json", - "url", + "windows-sys 0.61.0", ] [[package]] @@ -5323,17 +5233,21 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" dependencies = [ + "chrono", "dyn-clone", + "indexmap 2.11.4", "ref-cast", + "schemars_derive", "serde", "serde_json", + "url", ] [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -5343,9 +5257,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.22" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "5016d94c77c6d32f0b8e08b781f7dc8a90c2007d4e77472cc2807bc10a8438fe" dependencies = [ "proc-macro2", "quote", @@ -5414,7 +5328,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -5433,9 +5347,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" dependencies = [ "bitflags", "core-foundation", @@ -5446,9 +5360,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -5462,15 +5376,15 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "sentry" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030" +checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20" dependencies = [ "httpdate", "reqwest", @@ -5485,9 +5399,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e299dd3f7bcf676875eee852c9941e1d08278a743c32ca528e2debf846a653" +checksum = "f3253a495ab536f6de1746a58d5d7824b77d75e08e1a4b8ca6fb356839077ae0" dependencies = [ "backtrace", "regex", @@ -5496,9 +5410,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac0c5d6892cd4c414492fc957477b620026fb3411fca9fa12774831da561c88" +checksum = "027f81a728836e66b88c07666a10f5ed5a35e2695b04eb7aa0fcbed93f814900" dependencies = [ "hostname", "libc", @@ -5510,9 +5424,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaa38b94e70820ff3f1f9db3c8b0aef053b667be130f618e615e0ff2492cbcc" +checksum = "d3b6729c8e71ac968edbe9bf2dd4109c162e552b52bacd2b07e24ede1aba84a5" dependencies = [ "rand 0.9.2", "sentry-types", @@ -5523,9 +5437,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7a23b13c004873de3ce7db86eb0f59fe4adfc655a31f7bbc17fd10bacc9bfe" +checksum = "1ac0471f04f8f97af0c17eeca2c516e23faa1c0271a55bc64371d9ce488c2d40" dependencies = [ "sentry-backtrace", "sentry-core", @@ -5533,9 +5447,9 @@ dependencies = [ [[package]] name = "sentry-tower" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a303d0127d95ae928a937dcc0886931d28b4186e7338eea7d5786827b69b002" +checksum = "417bd48071863a65ca5f33d15af9aabd49a5cee7f97415d3f08ce8c90ed2c531" dependencies = [ "axum", "http", @@ -5548,9 +5462,9 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080" +checksum = "428f780866a613142dcc81b7f8551ae4d1c056f4df22b6d7ddd9154a9974eb03" dependencies = [ "bitflags", "sentry-backtrace", @@ -5561,16 +5475,16 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e477f4d4db08ddb4ab553717a8d3a511bc9e81dde0c808c680feacbb8105c412" +checksum = "2c19d1d1967b55659c358886d0f1aa3076488d445f84c7d727d384c675adaec1" dependencies = [ "debugid", "hex", "rand 0.9.2", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "url", "uuid", @@ -5578,18 +5492,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -5609,38 +5533,40 @@ dependencies = [ [[package]] name = "serde_html_form" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap 2.11.0", + "indexmap 2.11.4", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.11.4", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -5653,7 +5579,7 @@ dependencies = [ "futures", "percent-encoding", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -5687,9 +5613,9 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.0", + "indexmap 2.11.4", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde", "serde_derive", "serde_json", @@ -5715,7 +5641,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.11.4", "itoa", "ryu", "serde", @@ -5767,9 +5693,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -5812,13 +5738,24 @@ dependencies = [ ] [[package]] -name = "socket2" -version = "0.6.0" +name = "smartstring" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5855,7 +5792,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78222247fc55e10208ed1ba60f8296390bc67a489bc27a36231765d8d6f60ec5" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -5883,14 +5820,14 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener 5.4.0", + "event-listener 5.4.1", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "hashlink", - "indexmap 2.11.0", + "indexmap 2.11.4", "ipnetwork", "log", "memchr", @@ -5901,7 +5838,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -5986,7 +5923,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -6026,7 +5963,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -6052,7 +5989,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", "uuid", @@ -6136,9 +6073,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -6147,7 +6084,7 @@ dependencies = [ [[package]] name = "syn2mas" -version = "1.2.0-rc.0" +version = "1.6.0" dependencies = [ "anyhow", "arc-swap", @@ -6168,11 +6105,11 @@ dependencies = [ "opentelemetry-semantic-conventions", "rand 0.8.5", "rand_chacha 0.3.1", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "sqlx", - "thiserror 2.0.16", + "thiserror 2.0.17", "thiserror-ext", "tokio", "tokio-util", @@ -6204,21 +6141,21 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.0", ] [[package]] @@ -6241,11 +6178,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -6254,7 +6191,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fb7e61141f4141832ca9aad63c3c90023843f944a1975460abdacc64d03f534" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", "thiserror-ext-derive", ] @@ -6283,9 +6220,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -6303,12 +6240,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -6320,15 +6256,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -6346,9 +6282,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -6361,29 +6297,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -6392,9 +6325,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -6465,18 +6398,18 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.0", + "indexmap 2.11.4", "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.12", + "winnow 0.7.13", ] [[package]] name = "tonic" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", "base64", @@ -6486,13 +6419,24 @@ dependencies = [ "http-body-util", "percent-encoding", "pin-project", - "prost", + "sync_wrapper", "tokio-stream", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" @@ -6619,14 +6563,15 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" dependencies = [ "js-sys", - "once_cell", "opentelemetry", "opentelemetry_sdk", + "rustversion", + "thiserror 2.0.17", "tracing", "tracing-core", "tracing-subscriber", @@ -6651,17 +6596,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -6724,9 +6658,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" @@ -6827,9 +6761,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "js-sys", "serde", @@ -6931,11 +6865,20 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] @@ -6946,21 +6889,22 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", @@ -6972,9 +6916,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ "cfg-if", "js-sys", @@ -6985,9 +6929,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6995,9 +6939,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", @@ -7008,18 +6952,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" -version = "0.235.0" +version = "0.239.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" dependencies = [ "leb128fmt", "wasmparser", @@ -7027,22 +6971,22 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.235.0" +version = "0.239.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" dependencies = [ "bitflags", - "hashbrown 0.15.4", - "indexmap 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.11.4", "semver", "serde", ] [[package]] name = "wasmprinter" -version = "0.235.0" +version = "0.239.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75aa8e9076de6b9544e6dab4badada518cca0bf4966d35b131bbd057aed8fa0a" +checksum = "b3981f3d51f39f24f5fc90f93049a90f08dbbca8deba602cd46bb8ca67a94718" dependencies = [ "anyhow", "termcolor", @@ -7051,9 +6995,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe976922a16af3b0d67172c473d1fd4f1aa5d0af9c8ba6538c741f3af686f4" +checksum = "f81eafc07c867be94c47e0dc66355d9785e09107a18901f76a20701ba0663ad7" dependencies = [ "addr2line", "anyhow", @@ -7062,8 +7006,8 @@ dependencies = [ "bumpalo", "cc", "cfg-if", - "hashbrown 0.15.4", - "indexmap 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.11.4", "libc", "log", "mach2", @@ -7073,37 +7017,36 @@ dependencies = [ "postcard", "pulley-interpreter", "rayon", - "rustix 1.0.8", + "rustix", "serde", "serde_derive", "smallvec", "target-lexicon", - "trait-variant", "wasmparser", "wasmtime-environ", - "wasmtime-internal-asm-macros", "wasmtime-internal-component-macro", "wasmtime-internal-cranelift", "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", "wasmtime-internal-jit-icache-coherence", "wasmtime-internal-math", "wasmtime-internal-slab", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "wasmtime-environ" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44b6264a78d806924abbc76bbc75eac24976bc83bdfb938e5074ae551242436f" +checksum = "78587abe085a44a13c90fa16fea6db014e9883e627a7044d7f0cb397ad08d1da" dependencies = [ "anyhow", "cranelift-bitset", "cranelift-entity", "gimli", - "indexmap 2.11.0", + "indexmap 2.11.4", "log", "object", "postcard", @@ -7116,20 +7059,11 @@ dependencies = [ "wasmprinter", ] -[[package]] -name = "wasmtime-internal-asm-macros" -version = "35.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6775a9b516559716e5710e95a8014ca0adcc81e5bf4d3ad7899d89ae40094d1a" -dependencies = [ - "cfg-if", -] - [[package]] name = "wasmtime-internal-component-macro" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc3d098205e405e6b5ced06c1815621b823464b6ea289eaafe494139b0aee287" +checksum = "d843bb444f2d1509ea9304ad749242d1fa5de95cde67665bfcdcafa0f360925c" dependencies = [ "anyhow", "proc-macro2", @@ -7142,15 +7076,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219252067216242ed2b32665611b0ee356d6e92cbb897ecb9a10cae0b97bdeca" +checksum = "801ee1a80ab66f065a88c6a62f2d495d5540d027b366757c6a53e9c42f153aef" [[package]] name = "wasmtime-internal-cranelift" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec9ad7565e6a8de7cb95484e230ff689db74a4a085219e0da0cbd637a29c01c" +checksum = "deb50f1c50365c32e557266ca85acdf77696c44a3f98797ba6af58cebc6d6d1e" dependencies = [ "anyhow", "cfg-if", @@ -7166,61 +7100,71 @@ dependencies = [ "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 2.0.16", + "thiserror 2.0.17", "wasmparser", "wasmtime-environ", "wasmtime-internal-math", + "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-fiber" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b636ff8b220ebaf29dfe3b23770e4b2bad317b9683e3bf7345e162387385b39" +checksum = "9308cdb17f8d51e3164185616d809e28c29a6515c03b9dd95c89436b71f6d154" dependencies = [ "anyhow", "cc", "cfg-if", "libc", - "rustix 1.0.8", - "wasmtime-internal-asm-macros", + "rustix", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "38.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9b63a22bf2a8b6a149a41c6768bc17a8b2e3288a249cb8216987fbd7128e81" +dependencies = [ + "cc", "wasmtime-internal-versioned-export-macros", - "windows-sys 0.59.0", ] [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4417e06b7f80baff87d9770852c757a39b8d7f11d78b2620ca992b8725f16f50" +checksum = "eb8e042b6e3de2f3d708279f89f50b4b9aa1b9bab177300cdffb0ffcd2816df5" dependencies = [ "anyhow", "cfg-if", "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "wasmtime-internal-math" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7710d5c4ecdaa772927fd11e5dc30a9a62d1fc8fe933e11ad5576ad596ab6612" +checksum = "3c1f0674f38cd7d014eb1a49ea1d1766cca1a64459e8856ee118a10005302e16" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ab22fabe1eed27ab01fd47cd89deacf43ad222ed7fd169ba6f4dd1fbddc53b" +checksum = "fb24b7535306713e7a250f8b71e35f05b6a5031bf9c3ed7330c308e899cbe7d3" [[package]] name = "wasmtime-internal-unwinder" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307708f302f5dcf19c1bbbfb3d9f2cbc837dd18088a7988747b043a46ba38ecc" +checksum = "21d5a80e2623a49cb8e8c419542337b8fe0260b162c40dcc201080a84cbe9b7c" dependencies = [ "anyhow", "cfg-if", @@ -7231,9 +7175,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "342b0466f92b7217a4de9e114175fedee1907028567d2548bcd42f71a8b5b016" +checksum = "23e277f734b9256359b21517c3b0c26a2a9de6c53a51b670ae55cdcde548bf4e" dependencies = [ "proc-macro2", "quote", @@ -7242,21 +7186,22 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "35.0.0" +version = "38.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae057d44a5b60e6ec529b0c21809a9d1fc92e91ef6e0f6771ed11dd02a94a08" +checksum = "5f758625553fe33fdce0713f63bb7784c4f5fecb7f7cd4813414519ec24b6a4c" dependencies = [ "anyhow", + "bitflags", "heck 0.5.0", - "indexmap 2.11.0", + "indexmap 2.11.4", "wit-parser", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -7299,33 +7244,21 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "whoami" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall", + "libredox", "wasite", ] [[package]] name = "wildmatch" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" +checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382" [[package]] name = "winapi" @@ -7345,11 +7278,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -7360,13 +7293,13 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.2.0", "windows-result", "windows-strings", ] @@ -7400,21 +7333,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "windows-result" -version = "0.3.4" +name = "windows-link" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -7462,6 +7401,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -7514,7 +7462,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -7716,9 +7664,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -7747,23 +7695,20 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wit-parser" -version = "0.235.0" +version = "0.239.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1f95a87d03a33e259af286b857a95911eb46236a0f726cbaec1227b3dfc67a" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" dependencies = [ "anyhow", "id-arena", - "indexmap 2.11.0", + "indexmap 2.11.4", "log", "semver", "serde", @@ -7830,18 +7775,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -7871,9 +7816,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 77758f1a1..041088a0d 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.2.0-rc.0" +package.version = "1.6.0" package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial" package.authors = ["Element Backend Team"] package.edition = "2024" @@ -34,40 +34,40 @@ broken_intra_doc_links = "deny" [workspace.dependencies] # Workspace crates -mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.2.0-rc.0" } -mas-cli = { path = "./crates/cli/", version = "=1.2.0-rc.0" } -mas-config = { path = "./crates/config/", version = "=1.2.0-rc.0" } -mas-context = { path = "./crates/context/", version = "=1.2.0-rc.0" } -mas-data-model = { path = "./crates/data-model/", version = "=1.2.0-rc.0" } -mas-email = { path = "./crates/email/", version = "=1.2.0-rc.0" } -mas-graphql = { path = "./crates/graphql/", version = "=1.2.0-rc.0" } -mas-handlers = { path = "./crates/handlers/", version = "=1.2.0-rc.0" } -mas-http = { path = "./crates/http/", version = "=1.2.0-rc.0" } -mas-i18n = { path = "./crates/i18n/", version = "=1.2.0-rc.0" } -mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.2.0-rc.0" } -mas-iana = { path = "./crates/iana/", version = "=1.2.0-rc.0" } -mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.2.0-rc.0" } -mas-jose = { path = "./crates/jose/", version = "=1.2.0-rc.0" } -mas-keystore = { path = "./crates/keystore/", version = "=1.2.0-rc.0" } -mas-listener = { path = "./crates/listener/", version = "=1.2.0-rc.0" } -mas-matrix = { path = "./crates/matrix/", version = "=1.2.0-rc.0" } -mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.2.0-rc.0" } -mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.2.0-rc.0" } -mas-policy = { path = "./crates/policy/", version = "=1.2.0-rc.0" } -mas-router = { path = "./crates/router/", version = "=1.2.0-rc.0" } -mas-spa = { path = "./crates/spa/", version = "=1.2.0-rc.0" } -mas-storage = { path = "./crates/storage/", version = "=1.2.0-rc.0" } -mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.2.0-rc.0" } -mas-tasks = { path = "./crates/tasks/", version = "=1.2.0-rc.0" } -mas-templates = { path = "./crates/templates/", version = "=1.2.0-rc.0" } -mas-tower = { path = "./crates/tower/", version = "=1.2.0-rc.0" } -oauth2-types = { path = "./crates/oauth2-types/", version = "=1.2.0-rc.0" } -syn2mas = { path = "./crates/syn2mas", version = "=1.2.0-rc.0" } +mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.6.0" } +mas-cli = { path = "./crates/cli/", version = "=1.6.0" } +mas-config = { path = "./crates/config/", version = "=1.6.0" } +mas-context = { path = "./crates/context/", version = "=1.6.0" } +mas-data-model = { path = "./crates/data-model/", version = "=1.6.0" } +mas-email = { path = "./crates/email/", version = "=1.6.0" } +mas-graphql = { path = "./crates/graphql/", version = "=1.6.0" } +mas-handlers = { path = "./crates/handlers/", version = "=1.6.0" } +mas-http = { path = "./crates/http/", version = "=1.6.0" } +mas-i18n = { path = "./crates/i18n/", version = "=1.6.0" } +mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.6.0" } +mas-iana = { path = "./crates/iana/", version = "=1.6.0" } +mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.6.0" } +mas-jose = { path = "./crates/jose/", version = "=1.6.0" } +mas-keystore = { path = "./crates/keystore/", version = "=1.6.0" } +mas-listener = { path = "./crates/listener/", version = "=1.6.0" } +mas-matrix = { path = "./crates/matrix/", version = "=1.6.0" } +mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.6.0" } +mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.6.0" } +mas-policy = { path = "./crates/policy/", version = "=1.6.0" } +mas-router = { path = "./crates/router/", version = "=1.6.0" } +mas-spa = { path = "./crates/spa/", version = "=1.6.0" } +mas-storage = { path = "./crates/storage/", version = "=1.6.0" } +mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.6.0" } +mas-tasks = { path = "./crates/tasks/", version = "=1.6.0" } +mas-templates = { path = "./crates/templates/", version = "=1.6.0" } +mas-tower = { path = "./crates/tower/", version = "=1.6.0" } +oauth2-types = { path = "./crates/oauth2-types/", version = "=1.6.0" } +syn2mas = { path = "./crates/syn2mas", version = "=1.6.0" } # OpenAPI schema generation and validation [workspace.dependencies.aide] -version = "0.14.2" -features = ["axum", "axum-extra", "axum-json", "axum-query", "macros"] +version = "0.15.1" +features = ["axum", "axum-extra", "axum-extra-query", "axum-json", "macros"] # An `Arc` that can be atomically updated [workspace.dependencies.arc-swap] @@ -88,7 +88,7 @@ version = "0.1.89" # High-level error handling [workspace.dependencies.anyhow] -version = "1.0.99" +version = "1.0.100" # Assert that a value matches a pattern [workspace.dependencies.assert_matches] @@ -96,12 +96,12 @@ version = "1.5.0" # HTTP router [workspace.dependencies.axum] -version = "0.8.4" +version = "0.8.6" # Extra utilities for Axum [workspace.dependencies.axum-extra] -version = "0.10.1" -features = ["cookie-private", "cookie-key-expansion", "typed-header"] +version = "0.10.3" +features = ["cookie-private", "cookie-key-expansion", "typed-header", "query"] # Axum macros [workspace.dependencies.axum-macros] @@ -129,7 +129,7 @@ default-features = true # Packed bitfields [workspace.dependencies.bitflags] -version = "2.9.3" +version = "2.9.4" # Bytes [workspace.dependencies.bytes] @@ -137,7 +137,7 @@ version = "1.10.1" # UTF-8 paths [workspace.dependencies.camino] -version = "1.1.11" +version = "1.2.1" features = ["serde1"] # ChaCha20Poly1305 AEAD @@ -161,13 +161,13 @@ features = ["serde_json"] # Time utilities [workspace.dependencies.chrono] -version = "0.4.41" +version = "0.4.42" default-features = false features = ["serde", "clock"] # CLI argument parsing [workspace.dependencies.clap] -version = "4.5.46" +version = "4.5.50" features = ["derive"] # Object Identifiers (OIDs) as constants @@ -189,7 +189,7 @@ version = "0.15.0" # CSV parsing and writing [workspace.dependencies.csv] -version = "1.3.1" +version = "1.4.0" # DER encoding [workspace.dependencies.der] @@ -274,7 +274,7 @@ features = ["client", "server", "http1", "http2"] # Additional Hyper utilties [workspace.dependencies.hyper-util] -version = "0.1.16" +version = "0.1.17" features = [ "client", "server", @@ -321,7 +321,7 @@ features = ["std"] # HashMap which preserves insertion order [workspace.dependencies.indexmap] -version = "2.11.0" +version = "2.11.4" features = ["serde"] # Indented string literals @@ -330,13 +330,13 @@ version = "2.0.6" # Snapshot testing [workspace.dependencies.insta] -version = "1.43.1" +version = "1.43.2" features = ["yaml", "json"] # IP network address types [workspace.dependencies.ipnetwork] version = "0.20.0" -features = ["serde", "schemars"] +features = ["serde"] # Iterator utilities [workspace.dependencies.itertools] @@ -354,7 +354,7 @@ features = ["serde"] # Email sending [workspace.dependencies.lettre] -version = "0.11.18" +version = "0.11.19" default-features = false features = [ "tokio1-rustls", @@ -392,42 +392,40 @@ version = "0.3.0" # Open Policy Agent support through WASM [workspace.dependencies.opa-wasm] -version = "0.1.7" +version = "0.1.8" # OpenTelemetry [workspace.dependencies.opentelemetry] -version = "0.30.0" +version = "0.31.0" features = ["trace", "metrics"] [workspace.dependencies.opentelemetry-http] -version = "0.30.0" +version = "0.31.0" features = ["reqwest"] [workspace.dependencies.opentelemetry-jaeger-propagator] -version = "0.30.0" +version = "0.31.0" [workspace.dependencies.opentelemetry-otlp] -version = "0.30.0" +version = "0.31.0" default-features = false features = ["trace", "metrics", "http-proto"] -[workspace.dependencies.opentelemetry-prometheus] -# https://github.com/open-telemetry/opentelemetry-rust/pull/3076 -git = "https://github.com/sandhose/opentelemetry-rust.git" -branch = "otel-prometheus-0.30" +[workspace.dependencies.opentelemetry-prometheus-text-exporter] +version = "0.2.1" [workspace.dependencies.opentelemetry-resource-detectors] -version = "0.9.0" +version = "0.10.0" [workspace.dependencies.opentelemetry-semantic-conventions] -version = "0.30.0" +version = "0.31.0" features = ["semconv_experimental"] [workspace.dependencies.opentelemetry-stdout] -version = "0.30.0" +version = "0.31.0" features = ["trace", "metrics"] [workspace.dependencies.opentelemetry_sdk] -version = "0.30.0" +version = "0.31.0" features = [ "experimental_trace_batch_span_processor_with_async_runtime", "experimental_metrics_periodicreader_with_async_runtime", "rt-tokio", ] [workspace.dependencies.tracing-opentelemetry] -version = "0.31.0" +version = "0.32.0" default-features = false # P256 elliptic curve @@ -456,11 +454,11 @@ features = ["std"] # Parser generator [workspace.dependencies.pest] -version = "2.8.1" +version = "2.8.3" # Pest derive macros [workspace.dependencies.pest_derive] -version = "2.8.1" +version = "2.8.3" # Pin projection [workspace.dependencies.pin-project-lite] @@ -478,11 +476,7 @@ features = ["std", "pkcs5", "encryption"] # Public Suffix List [workspace.dependencies.psl] -version = "2.1.136" - -# Prometheus metrics -[workspace.dependencies.prometheus] -version = "0.14.0" +version = "2.1.162" # High-precision clock [workspace.dependencies.quanta] @@ -498,11 +492,11 @@ version = "0.6.4" # Regular expressions [workspace.dependencies.regex] -version = "1.11.2" +version = "1.12.2" # High-level HTTP client [workspace.dependencies.reqwest] -version = "0.12.23" +version = "0.12.24" default-features = false features = [ "http2", @@ -523,11 +517,11 @@ version = "2.1.1" # Matrix-related types [workspace.dependencies.ruma-common] -version = "0.15.4" +version = "0.16.0" # TLS stack [workspace.dependencies.rustls] -version = "0.23.31" +version = "0.23.35" # PEM parsing for rustls [workspace.dependencies.rustls-pemfile] @@ -535,7 +529,7 @@ version = "2.2.0" # PKI types for rustls [workspace.dependencies.rustls-pki-types] -version = "1.12.0" +version = "1.13.0" # Use platform-specific verifier for TLS [workspace.dependencies.rustls-platform-verifier] @@ -547,8 +541,8 @@ version = "0.4.5" # JSON Schema generation [workspace.dependencies.schemars] -version = "0.8.22" -features = ["url", "chrono", "preserve_order"] +version = "0.9.0" +features = ["url2", "chrono04", "preserve_order"] # SEC1 encoding format [workspace.dependencies.sec1] @@ -573,27 +567,27 @@ features = [ # Sentry error tracking [workspace.dependencies.sentry] -version = "0.42.0" +version = "0.45.0" default-features = false features = ["backtrace", "contexts", "panic", "tower", "reqwest"] # Sentry tower layer [workspace.dependencies.sentry-tower] -version = "0.42.0" +version = "0.45.0" features = ["http", "axum-matched-path"] # Sentry tracing integration [workspace.dependencies.sentry-tracing] -version = "0.42.0" +version = "0.45.0" # Serialization and deserialization [workspace.dependencies.serde] -version = "1.0.219" +version = "1.0.228" features = ["derive"] # Most of the time, if we need serde, we need derive # JSON serialization and deserialization [workspace.dependencies.serde_json] -version = "1.0.143" +version = "1.0.145" features = ["preserve_order"] # URL encoded form serialization @@ -620,7 +614,7 @@ version = "2.2.0" # Low-level socket manipulation [workspace.dependencies.socket2] -version = "0.6.0" +version = "0.6.1" # Subject Public Key Info [workspace.dependencies.spki] @@ -643,14 +637,14 @@ features = [ # Custom error types [workspace.dependencies.thiserror] -version = "2.0.16" +version = "2.0.17" [workspace.dependencies.thiserror-ext] version = "0.3.0" # Async runtime [workspace.dependencies.tokio] -version = "1.47.1" +version = "1.48.0" features = ["full"] [workspace.dependencies.tokio-stream] @@ -658,7 +652,7 @@ version = "0.1.17" # Tokio rustls integration [workspace.dependencies.tokio-rustls] -version = "0.26.2" +version = "0.26.4" # Tokio test utilities [workspace.dependencies.tokio-test] @@ -712,7 +706,7 @@ features = ["serde", "uuid"] # UUID support [workspace.dependencies.uuid] -version = "1.18.0" +version = "1.18.1" # HTML escaping [workspace.dependencies.v_htmlescape] @@ -741,7 +735,7 @@ version = "0.5.5" # Zero memory after use [workspace.dependencies.zeroize] -version = "1.8.1" +version = "1.8.2" # Password strength estimation [workspace.dependencies.zxcvbn] diff --git a/Dockerfile b/Dockerfile index 14eda1924..479dee308 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,10 @@ ARG DEBIAN_VERSION=12 ARG DEBIAN_VERSION_NAME=bookworm ARG RUSTC_VERSION=1.89.0 -ARG NODEJS_VERSION=20.15.0 -ARG OPA_VERSION=1.1.0 -ARG CARGO_AUDITABLE_VERSION=0.6.6 +ARG NODEJS_VERSION=24.11.0 +# Keep in sync with .github/actions/build-policies/action.yml and policies/Makefile +ARG OPA_VERSION=1.8.0 +ARG CARGO_AUDITABLE_VERSION=0.7.0 ########################################## ## Build stage that builds the frontend ## @@ -24,7 +25,7 @@ FROM --platform=${BUILDPLATFORM} docker.io/library/node:${NODEJS_VERSION}-${DEBI WORKDIR /app/frontend -COPY ./frontend/package.json ./frontend/package-lock.json /app/frontend/ +COPY ./frontend/.npmrc ./frontend/package.json ./frontend/package-lock.json /app/frontend/ # Network access: to fetch dependencies RUN --network=default \ npm ci diff --git a/biome.json b/biome.json index 9dc190833..cc5454160 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "vcs": { "enabled": true, @@ -32,6 +32,12 @@ "enabled": true, "rules": { "recommended": true, + "complexity": { + "noImportantStyles": "off" + }, + "suspicious": { + "noUnknownAtRules": "off" + }, "correctness": { "noUnusedImports": "warn", "noUnusedVariables": "warn" diff --git a/clippy.toml b/clippy.toml index 218811441..db1ba69dc 100644 --- a/clippy.toml +++ b/clippy.toml @@ -17,4 +17,6 @@ disallowed-methods = [ disallowed-types = [ { path = "std::path::PathBuf", reason = "use camino::Utf8PathBuf instead" }, { path = "std::path::Path", reason = "use camino::Utf8Path instead" }, + { path = "axum::extract::Query", reason = "use axum_extra::extract::Query instead. The built-in version doesn't deserialise lists."}, + { path = "axum::extract::rejection::QueryRejection", reason = "use axum_extra::extract::QueryRejection instead"} ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 22def30c7..1baebd12c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -59,12 +59,11 @@ opentelemetry.workspace = true opentelemetry-http.workspace = true opentelemetry-jaeger-propagator.workspace = true opentelemetry-otlp.workspace = true -opentelemetry-prometheus.workspace = true +opentelemetry-prometheus-text-exporter.workspace = true opentelemetry-resource-detectors.workspace = true opentelemetry-semantic-conventions.workspace = true opentelemetry-stdout.workspace = true opentelemetry_sdk.workspace = true -prometheus.workspace = true sentry.workspace = true sentry-tracing.workspace = true sentry-tower.workspace = true diff --git a/crates/cli/src/app_state.rs b/crates/cli/src/app_state.rs index f9f761338..f211fc29c 100644 --- a/crates/cli/src/app_state.rs +++ b/crates/cli/src/app_state.rs @@ -9,7 +9,7 @@ use std::{convert::Infallible, net::IpAddr, sync::Arc}; use axum::extract::{FromRef, FromRequestParts}; use ipnetwork::IpNetwork; use mas_context::LogContext; -use mas_data_model::{BoxClock, BoxRng, SiteConfig, SystemClock}; +use mas_data_model::{AppVersion, BoxClock, BoxRng, SiteConfig, SystemClock}; use mas_handlers::{ ActivityTracker, BoundActivityTracker, CookieManager, ErrorWrapper, GraphQLSchema, Limiter, MetadataCache, RequesterFingerprint, passwords::PasswordManager, @@ -27,7 +27,7 @@ use rand::SeedableRng; use sqlx::PgPool; use tracing::Instrument; -use crate::telemetry::METER; +use crate::{VERSION, telemetry::METER}; #[derive(Clone)] pub struct AppState { @@ -214,6 +214,12 @@ impl FromRef for Arc { } } +impl FromRef for AppVersion { + fn from_ref(_input: &AppState) -> Self { + AppVersion(VERSION) + } +} + impl FromRequestParts for BoxClock { type Rejection = Infallible; diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index a6a8e67a0..5bbd870ea 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -19,14 +19,17 @@ use mas_data_model::{Clock, Device, SystemClock, TokenType, Ulid, UpstreamOAuthP use mas_email::Address; use mas_matrix::HomeserverConnection; use mas_storage::{ - RepositoryAccess, + Pagination, RepositoryAccess, compat::{CompatAccessTokenRepository, CompatSessionFilter, CompatSessionRepository}, oauth2::OAuth2SessionFilter, queue::{ DeactivateUserJob, ProvisionUserJob, QueueJobRepositoryExt as _, ReactivateUserJob, SyncDevicesJob, }, - user::{BrowserSessionFilter, UserEmailRepository, UserPasswordRepository, UserRepository}, + user::{ + BrowserSessionFilter, UserEmailRepository, UserFilter, UserPasswordRepository, + UserRepository, + }, }; use mas_storage_pg::{DatabaseError, PgRepository}; use rand::{ @@ -85,6 +88,15 @@ enum Subcommand { ignore_complexity: bool, }, + /// Make a user admin + PromoteAdmin { username: String }, + + /// Make a user non-admin + DemoteAdmin { username: String }, + + /// List all users with admin privileges + ListAdminUsers, + /// Issue a compatibility token IssueCompatibilityToken { /// User for which to issue the token @@ -315,6 +327,83 @@ impl Options { Ok(ExitCode::SUCCESS) } + SC::PromoteAdmin { username } => { + let _span = + info_span!("cli.manage.promote_admin", user.username = username,).entered(); + + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let mut conn = database_connection_from_config(&database_config).await?; + let txn = conn.begin().await?; + let mut repo = PgRepository::from_conn(txn); + + let user = repo + .user() + .find_by_username(&username) + .await? + .context("User not found")?; + + let user = repo.user().set_can_request_admin(user, true).await?; + + repo.into_inner().commit().await?; + info!(%user.id, %user.username, "User promoted to admin"); + + Ok(ExitCode::SUCCESS) + } + + SC::DemoteAdmin { username } => { + let _span = + info_span!("cli.manage.demote_admin", user.username = username,).entered(); + + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let mut conn = database_connection_from_config(&database_config).await?; + let txn = conn.begin().await?; + let mut repo = PgRepository::from_conn(txn); + + let user = repo + .user() + .find_by_username(&username) + .await? + .context("User not found")?; + + let user = repo.user().set_can_request_admin(user, false).await?; + + repo.into_inner().commit().await?; + info!(%user.id, %user.username, "User is no longer admin"); + + Ok(ExitCode::SUCCESS) + } + + SC::ListAdminUsers => { + let _span = info_span!("cli.manage.list_admins").entered(); + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let mut conn = database_connection_from_config(&database_config).await?; + let txn = conn.begin().await?; + let mut repo = PgRepository::from_conn(txn); + + let mut cursor = Pagination::first(1000); + let filter = UserFilter::new().can_request_admin_only(); + let total = repo.user().count(filter).await?; + + info!("The following users can request admin privileges ({total} total):"); + loop { + let page = repo.user().list(filter, cursor).await?; + for edge in page.edges { + let user = edge.node; + info!(%user.id, username = %user.username); + cursor = cursor.after(edge.cursor); + } + + if !page.has_next_page { + break; + } + } + + Ok(ExitCode::SUCCESS) + } + SC::IssueCompatibilityToken { username, admin, diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 1367f131e..52465f077 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -160,8 +160,14 @@ impl Options { )?; // Load and compile the templates - let templates = - templates_from_config(&config.templates, &site_config, &url_builder).await?; + let templates = templates_from_config( + &config.templates, + &site_config, + &url_builder, + // Don't use strict mode in production yet + false, + ) + .await?; shutdown.register_reloadable(&templates); let http_client = mas_http::reqwest_client(); diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index 111f19682..8f1b0dd4e 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -4,8 +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::process::ExitCode; +use std::{fmt::Write, process::ExitCode}; +use anyhow::{Context as _, bail}; +use camino::Utf8PathBuf; use clap::Parser; use figment::Figment; use mas_config::{ @@ -27,14 +29,19 @@ pub(super) struct Options { #[derive(Parser, Debug)] enum Subcommand { /// Check that the templates specified in the config are valid - Check, + Check { + /// If set, templates will be rendered to this directory. + /// The directory must either not exist or be empty. + #[arg(long = "out-dir")] + out_dir: Option, + }, } impl Options { pub async fn run(self, figment: &Figment) -> anyhow::Result { use Subcommand as SC; match self.subcommand { - SC::Check => { + SC::Check { out_dir } => { let _span = info_span!("cli.templates.check").entered(); let template_config = TemplatesConfig::extract_or_default(figment) @@ -65,9 +72,54 @@ impl Options { &account_config, &captcha_config, )?; - let templates = - templates_from_config(&template_config, &site_config, &url_builder).await?; - templates.check_render(clock.now(), &mut rng)?; + let templates = templates_from_config( + &template_config, + &site_config, + &url_builder, // Use strict mode in template checks + true, + ) + .await?; + let all_renders = templates.check_render(clock.now(), &mut rng)?; + + if let Some(out_dir) = out_dir { + // Save renders to disk. + if out_dir.exists() { + let mut read_dir = + tokio::fs::read_dir(&out_dir).await.with_context(|| { + format!("could not read {out_dir} to check it's empty") + })?; + if read_dir.next_entry().await?.is_some() { + bail!("Render directory {out_dir} is not empty, refusing to write."); + } + } else { + tokio::fs::create_dir(&out_dir) + .await + .with_context(|| format!("could not create {out_dir}"))?; + } + + for ((template, sample_identifier), template_render) in &all_renders { + let (template_filename_base, template_ext) = + template.rsplit_once('.').unwrap_or((template, "txt")); + let template_filename_base = template_filename_base.replace('/', "_"); + + // Make a string like `-index=0-browser-session=0-locale=fr` + let sample_suffix = { + let mut s = String::new(); + for (k, v) in &sample_identifier.components { + write!(s, "-{k}={v}")?; + } + s + }; + + let render_path = out_dir.join(format!( + "{template_filename_base}{sample_suffix}.{template_ext}" + )); + + tokio::fs::write(&render_path, template_render.as_bytes()) + .await + .with_context(|| format!("could not write render to {render_path}"))?; + } + } Ok(ExitCode::SUCCESS) } diff --git a/crates/cli/src/commands/worker.rs b/crates/cli/src/commands/worker.rs index 31d0d56c2..a1eb0fcce 100644 --- a/crates/cli/src/commands/worker.rs +++ b/crates/cli/src/commands/worker.rs @@ -52,8 +52,14 @@ impl Options { )?; // Load and compile the templates - let templates = - templates_from_config(&config.templates, &site_config, &url_builder).await?; + let templates = templates_from_config( + &config.templates, + &site_config, + &url_builder, + // Don't use strict mode on task workers for now + false, + ) + .await?; let mailer = mailer_from_config(&config.email, &templates)?; test_mailer_in_background(&mailer, Duration::from_secs(30)); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 5df40da83..9c1121cca 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -149,12 +149,14 @@ async fn try_main() -> anyhow::Result { // Setup OpenTelemetry tracing and metrics self::telemetry::setup(&telemetry_config).context("failed to setup OpenTelemetry")?; - let telemetry_layer = self::telemetry::TRACER.get().map(|tracer| { - tracing_opentelemetry::layer() - .with_tracer(tracer.clone()) - .with_tracked_inactivity(false) - .with_filter(LevelFilter::INFO) - }); + let tracer = self::telemetry::TRACER + .get() + .context("TRACER was not set")?; + + let telemetry_layer = tracing_opentelemetry::layer() + .with_tracer(tracer.clone()) + .with_tracked_inactivity(false) + .with_filter(LevelFilter::INFO); let subscriber = Registry::default() .with(suppress_layer) diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 9ce9b3a52..58d3672e0 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -136,14 +136,24 @@ fn make_http_span(req: &Request) -> Span { span.record(USER_AGENT_ORIGINAL, user_agent); } - // Extract the parent span context from the request headers - let parent_context = opentelemetry::global::get_text_map_propagator(|propagator| { - let extractor = HeaderExtractor(req.headers()); - let context = opentelemetry::Context::new(); - propagator.extract_with_context(&context, &extractor) - }); + // In case the span is disabled by any of tracing layers, e.g. if `RUST_LOG` + // is set to `warn`, `set_parent` will fail. So we only try to set the + // parent context if the span is not disabled. + if !span.is_disabled() { + // Extract the parent span context from the request headers + let parent_context = opentelemetry::global::get_text_map_propagator(|propagator| { + let extractor = HeaderExtractor(req.headers()); + let context = opentelemetry::Context::new(); + propagator.extract_with_context(&context, &extractor) + }); - span.set_parent(parent_context); + if let Err(err) = span.set_parent(parent_context) { + tracing::error!( + error = &err as &dyn std::error::Error, + "Failed to set parent context on span" + ); + } + } span } diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index 4b8c388c3..1aa8f7402 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -132,7 +132,8 @@ pub async fn config_sync( let mut existing_enabled_ids = BTreeSet::new(); let mut existing_disabled = BTreeMap::new(); // Process the existing providers - for provider in page.edges { + for edge in page.edges { + let provider = edge.node; if provider.enabled() { if config_ids.contains(&provider.id) { existing_enabled_ids.insert(provider.id); @@ -201,25 +202,24 @@ pub async fn config_sync( continue; } - let encrypted_client_secret = - if let Some(client_secret) = provider.client_secret.as_deref() { - Some(encrypter.encrypt_to_string(client_secret.as_bytes())?) - } else if let Some(mut siwa) = provider.sign_in_with_apple.clone() { - // if private key file is defined and not private key (raw), we populate the - // private key to hold the content of the private key file. - // private key (raw) takes precedence so both can be defined - // without issues - if siwa.private_key.is_none() - && let Some(private_key_file) = siwa.private_key_file.take() - { - let key = tokio::fs::read_to_string(private_key_file).await?; - siwa.private_key = Some(key); - } - let encoded = serde_json::to_vec(&siwa)?; - Some(encrypter.encrypt_to_string(&encoded)?) - } else { - None - }; + let encrypted_client_secret = if let Some(client_secret) = provider.client_secret { + Some(encrypter.encrypt_to_string(client_secret.value().await?.as_bytes())?) + } else if let Some(mut siwa) = provider.sign_in_with_apple.clone() { + // if private key file is defined and not private key (raw), we populate the + // private key to hold the content of the private key file. + // private key (raw) takes precedence so both can be defined + // without issues + if siwa.private_key.is_none() + && let Some(private_key_file) = siwa.private_key_file.take() + { + let key = tokio::fs::read_to_string(private_key_file).await?; + siwa.private_key = Some(key); + } + let encoded = serde_json::to_vec(&siwa)?; + Some(encrypter.encrypt_to_string(&encoded)?) + } else { + None + }; let discovery_mode = match provider.discovery_mode { mas_config::UpstreamOAuth2DiscoveryMode::Oidc => { diff --git a/crates/cli/src/telemetry.rs b/crates/cli/src/telemetry.rs index 630dcbfca..54222c8be 100644 --- a/crates/cli/src/telemetry.rs +++ b/crates/cli/src/telemetry.rs @@ -23,18 +23,17 @@ use opentelemetry::{ trace::TracerProvider as _, }; use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; -use opentelemetry_prometheus::PrometheusExporter; +use opentelemetry_prometheus_text_exporter::PrometheusExporter; use opentelemetry_sdk::{ Resource, metrics::{ManualReader, SdkMeterProvider, periodic_reader_with_async_runtime::PeriodicReader}, propagation::{BaggagePropagator, TraceContextPropagator}, trace::{ - Sampler, SdkTracerProvider, Tracer, span_processor_with_async_runtime::BatchSpanProcessor, + IdGenerator, Sampler, SdkTracerProvider, Tracer, + span_processor_with_async_runtime::BatchSpanProcessor, }, }; use opentelemetry_semantic_conventions as semcov; -use prometheus::Registry; -use url::Url; static SCOPE: LazyLock = LazyLock::new(|| { InstrumentationScope::builder(env!("CARGO_PKG_NAME")) @@ -49,7 +48,7 @@ pub static METER: LazyLock = pub static TRACER: OnceLock = OnceLock::new(); static METER_PROVIDER: OnceLock = OnceLock::new(); static TRACER_PROVIDER: OnceLock = OnceLock::new(); -static PROMETHEUS_REGISTRY: OnceLock = OnceLock::new(); +static PROMETHEUS_EXPORTER: OnceLock = OnceLock::new(); pub fn setup(config: &TelemetryConfig) -> anyhow::Result<()> { let propagator = propagator(&config.tracing.propagators); @@ -95,50 +94,65 @@ fn propagator(propagators: &[Propagator]) -> TextMapCompositePropagator { TextMapCompositePropagator::new(propagators) } -fn stdout_tracer_provider() -> SdkTracerProvider { - let exporter = opentelemetry_stdout::SpanExporter::default(); - SdkTracerProvider::builder() - .with_simple_exporter(exporter) - .build() +/// An [`IdGenerator`] which always returns an invalid trace ID and span ID +/// +/// This is used when no exporter is being used, so that we don't log the trace +/// ID when we're not tracing. +#[derive(Debug, Clone, Copy)] +struct InvalidIdGenerator; +impl IdGenerator for InvalidIdGenerator { + fn new_trace_id(&self) -> opentelemetry::TraceId { + opentelemetry::TraceId::INVALID + } + fn new_span_id(&self) -> opentelemetry::SpanId { + opentelemetry::SpanId::INVALID + } } -fn otlp_tracer_provider( - endpoint: Option<&Url>, - sample_rate: f64, -) -> anyhow::Result { - let mut exporter = opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_http_client(mas_http::reqwest_client()); - if let Some(endpoint) = endpoint { - exporter = exporter.with_endpoint(endpoint.to_string()); - } - let exporter = exporter - .build() - .context("Failed to configure OTLP trace exporter")?; - - let batch_processor = - BatchSpanProcessor::builder(exporter, opentelemetry_sdk::runtime::Tokio).build(); +fn init_tracer(config: &TracingConfig) -> anyhow::Result<()> { + let sample_rate = config.sample_rate.unwrap_or(1.0); // We sample traces based on the parent if we have one, and if not, we // sample a ratio based on the configured sample rate let sampler = Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(sample_rate))); - let tracer_provider = SdkTracerProvider::builder() - .with_span_processor(batch_processor) + let tracer_provider_builder = SdkTracerProvider::builder() .with_resource(resource()) - .with_sampler(sampler) - .build(); + .with_sampler(sampler); - Ok(tracer_provider) -} - -fn init_tracer(config: &TracingConfig) -> anyhow::Result<()> { - let sample_rate = config.sample_rate.unwrap_or(1.0); let tracer_provider = match config.exporter { - TracingExporterKind::None => return Ok(()), - TracingExporterKind::Stdout => stdout_tracer_provider(), - TracingExporterKind::Otlp => otlp_tracer_provider(config.endpoint.as_ref(), sample_rate)?, + TracingExporterKind::None => tracer_provider_builder + .with_id_generator(InvalidIdGenerator) + .with_sampler(Sampler::AlwaysOff) + .build(), + + TracingExporterKind::Stdout => { + let exporter = opentelemetry_stdout::SpanExporter::default(); + tracer_provider_builder + .with_simple_exporter(exporter) + .build() + } + + TracingExporterKind::Otlp => { + let mut exporter = opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_http_client(mas_http::reqwest_client()); + if let Some(endpoint) = &config.endpoint { + exporter = exporter.with_endpoint(endpoint.as_str()); + } + let exporter = exporter + .build() + .context("Failed to configure OTLP trace exporter")?; + + let batch_processor = + BatchSpanProcessor::builder(exporter, opentelemetry_sdk::runtime::Tokio).build(); + + tracer_provider_builder + .with_span_processor(batch_processor) + .build() + } }; + TRACER_PROVIDER .set(tracer_provider.clone()) .map_err(|_| anyhow::anyhow!("TRACER_PROVIDER was set twice"))?; @@ -180,21 +194,30 @@ type PromServiceFuture = #[allow(clippy::needless_pass_by_value)] fn prometheus_service_fn(_req: T) -> PromServiceFuture { - use prometheus::{Encoder, TextEncoder}; + let response = if let Some(exporter) = PROMETHEUS_EXPORTER.get() { + // We'll need some space for this, so we preallocate a bit + let mut buffer = Vec::with_capacity(1024); - let response = if let Some(registry) = PROMETHEUS_REGISTRY.get() { - let mut buffer = Vec::new(); - let encoder = TextEncoder::new(); - let metric_families = registry.gather(); + if let Err(err) = exporter.export(&mut buffer) { + tracing::error!( + error = &err as &dyn std::error::Error, + "Failed to export Prometheus metrics" + ); - // That shouldn't panic, unless we're constructing invalid labels - encoder.encode(&metric_families, &mut buffer).unwrap(); - - Response::builder() - .status(200) - .header(CONTENT_TYPE, encoder.format_type()) - .body(Full::new(Bytes::from(buffer))) - .unwrap() + Response::builder() + .status(500) + .header(CONTENT_TYPE, "text/plain") + .body(Full::new(Bytes::from_static( + b"Failed to export Prometheus metrics, see logs for details", + ))) + .unwrap() + } else { + Response::builder() + .status(200) + .header(CONTENT_TYPE, "text/plain;version=1.0.0") + .body(Full::new(Bytes::from(buffer))) + .unwrap() + } } else { Response::builder() .status(500) @@ -209,7 +232,7 @@ fn prometheus_service_fn(_req: T) -> PromServiceFuture { } pub fn prometheus_service() -> tower::util::ServiceFn PromServiceFuture> { - if PROMETHEUS_REGISTRY.get().is_none() { + if PROMETHEUS_EXPORTER.get().is_none() { tracing::warn!( "A Prometheus resource was mounted on a listener, but the Prometheus exporter was not setup in the config" ); @@ -219,16 +242,11 @@ pub fn prometheus_service() -> tower::util::ServiceFn PromServiceFut } fn prometheus_metric_reader() -> anyhow::Result { - let registry = Registry::new(); + let exporter = PrometheusExporter::builder().without_scope_info().build(); - PROMETHEUS_REGISTRY - .set(registry.clone()) - .map_err(|_| anyhow::anyhow!("PROMETHEUS_REGISTRY was set twice"))?; - - let exporter = opentelemetry_prometheus::exporter() - .with_registry(registry) - .without_scope_info() - .build()?; + PROMETHEUS_EXPORTER + .set(exporter.clone()) + .map_err(|_| anyhow::anyhow!("PROMETHEUS_EXPORTER was set twice"))?; Ok(exporter) } diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 959c8ba0f..4925d9866 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -211,6 +211,7 @@ pub fn site_config_from_config( password_login_enabled: password_config.enabled(), password_registration_enabled: password_config.enabled() && account_config.password_registration_enabled, + password_registration_email_required: account_config.password_registration_email_required, registration_token_required: account_config.registration_token_required, email_change_allowed: account_config.email_change_allowed, displayname_change_allowed: account_config.displayname_change_allowed, @@ -231,6 +232,7 @@ pub async fn templates_from_config( config: &TemplatesConfig, site_config: &SiteConfig, url_builder: &UrlBuilder, + strict: bool, ) -> Result { Templates::load( config.path.clone(), @@ -239,6 +241,7 @@ pub async fn templates_from_config( config.translations_path.clone(), site_config.templates_branding(), site_config.templates_features(), + strict, ) .await .with_context(|| format!("Failed to load the templates at {}", config.path)) diff --git a/crates/config/src/bin/schema.rs b/crates/config/src/bin/schema.rs index 83a28c8c9..db5ac230f 100644 --- a/crates/config/src/bin/schema.rs +++ b/crates/config/src/bin/schema.rs @@ -4,14 +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 schemars::r#gen::SchemaSettings; +use schemars::generate::SchemaSettings; fn main() { - let settings = SchemaSettings::draft07().with(|s| { - s.option_nullable = false; - s.option_add_null_type = false; - }); - let generator = settings.into_generator(); + let generator = SchemaSettings::draft07().into_generator(); let schema = generator.into_root_schema_for::(); serde_json::to_writer_pretty(std::io::stdout(), &schema).expect("Failed to serialize schema"); diff --git a/crates/config/src/schema.rs b/crates/config/src/schema.rs index a3c732419..7c1761e30 100644 --- a/crates/config/src/schema.rs +++ b/crates/config/src/schema.rs @@ -6,29 +6,22 @@ //! Useful JSON Schema definitions -use schemars::{ - JsonSchema, - r#gen::SchemaGenerator, - schema::{InstanceType, Schema, SchemaObject}, -}; +use std::borrow::Cow; + +use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema}; /// A network hostname pub struct Hostname; impl JsonSchema for Hostname { - fn schema_name() -> String { - "Hostname".to_string() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("Hostname") } - fn json_schema(generator: &mut SchemaGenerator) -> Schema { - hostname(generator) + fn json_schema(_generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "string", + "format": "hostname", + }) } } - -fn hostname(_gen: &mut SchemaGenerator) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("hostname".to_owned()), - ..SchemaObject::default() - }) -} diff --git a/crates/config/src/sections/account.rs b/crates/config/src/sections/account.rs index 47efa0162..2b6538a2b 100644 --- a/crates/config/src/sections/account.rs +++ b/crates/config/src/sections/account.rs @@ -50,6 +50,13 @@ pub struct AccountConfig { #[serde(default = "default_false", skip_serializing_if = "is_default_false")] pub password_registration_enabled: bool, + /// Whether self-service password registrations require a valid email. + /// Defaults to `true`. + /// + /// This has no effect if password registration is disabled. + #[serde(default = "default_true", skip_serializing_if = "is_default_true")] + pub password_registration_email_required: bool, + /// Whether users are allowed to change their passwords. Defaults to `true`. /// /// This has no effect if password login is disabled. @@ -89,6 +96,7 @@ impl Default for AccountConfig { email_change_allowed: default_true(), displayname_change_allowed: default_true(), password_registration_enabled: default_false(), + password_registration_email_required: default_true(), password_change_allowed: default_true(), password_recovery_enabled: default_false(), account_deactivation_allowed: default_true(), diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 0951ebba2..8b9120045 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -6,8 +6,6 @@ use std::ops::Deref; -use anyhow::bail; -use camino::Utf8PathBuf; use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_jose::jwk::PublicJsonWebKeySet; use schemars::JsonSchema; @@ -16,7 +14,7 @@ use serde_with::serde_as; use ulid::Ulid; use url::Url; -use super::ConfigurationSection; +use super::{ClientSecret, ClientSecretRaw, ConfigurationSection}; #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "snake_case")] @@ -31,66 +29,6 @@ impl From for JwksOrJwksUri { } } -/// Client secret config option. -/// -/// It either holds the client secret value directly or references a file where -/// the client secret is stored. -#[derive(Clone, Debug)] -pub enum ClientSecret { - File(Utf8PathBuf), - Value(String), -} - -/// Client secret fields as serialized in JSON. -#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] -struct ClientSecretRaw { - /// Path to the file containing the client secret. The client secret is used - /// by the `client_secret_basic`, `client_secret_post` and - /// `client_secret_jwt` authentication methods. - #[schemars(with = "Option")] - #[serde(skip_serializing_if = "Option::is_none")] - client_secret_file: Option, - - /// Alternative to `client_secret_file`: Reads the client secret directly - /// from the config. - #[serde(skip_serializing_if = "Option::is_none")] - client_secret: Option, -} - -impl TryFrom for Option { - type Error = anyhow::Error; - - fn try_from(value: ClientSecretRaw) -> Result { - match (value.client_secret, value.client_secret_file) { - (None, None) => Ok(None), - (None, Some(path)) => Ok(Some(ClientSecret::File(path))), - (Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))), - (Some(_), Some(_)) => { - bail!("Cannot specify both `client_secret` and `client_secret_file`") - } - } - } -} - -impl From> for ClientSecretRaw { - fn from(value: Option) -> Self { - match value { - Some(ClientSecret::File(path)) => ClientSecretRaw { - client_secret_file: Some(path), - client_secret: None, - }, - Some(ClientSecret::Value(client_secret)) => ClientSecretRaw { - client_secret_file: None, - client_secret: Some(client_secret), - }, - None => ClientSecretRaw { - client_secret_file: None, - client_secret: None, - }, - } - } -} - /// Authentication method used by clients #[derive(JsonSchema, Serialize, Deserialize, Copy, Clone, Debug)] #[serde(rename_all = "snake_case")] @@ -273,8 +211,7 @@ impl ClientConfig { /// Returns an error when the client secret could not be read from file. pub async fn client_secret(&self) -> anyhow::Result> { Ok(match &self.client_secret { - Some(ClientSecret::File(path)) => Some(tokio::fs::read_to_string(path).await?), - Some(ClientSecret::Value(client_secret)) => Some(client_secret.clone()), + Some(client_secret) => Some(client_secret.value().await?), None => None, }) } diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index c75d47fd3..c01b8eb0d 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -23,19 +23,6 @@ fn default_public_base() -> Url { "http://[::]:8080".parse().unwrap() } -fn http_address_example_1() -> &'static str { - "[::1]:8080" -} -fn http_address_example_2() -> &'static str { - "[::]:8080" -} -fn http_address_example_3() -> &'static str { - "127.0.0.1:8080" -} -fn http_address_example_4() -> &'static str { - "0.0.0.0:8080" -} - #[cfg(not(any(feature = "docker", feature = "dist")))] fn http_listener_assets_path_default() -> Utf8PathBuf { "./frontend/dist/".into() @@ -111,10 +98,10 @@ pub enum BindConfig { Address { /// Host and port on which to listen #[schemars( - example = "http_address_example_1", - example = "http_address_example_2", - example = "http_address_example_3", - example = "http_address_example_4" + example = &"[::1]:8080", + example = &"[::]:8080", + example = &"127.0.0.1:8080", + example = &"0.0.0.0:8080", )] address: String, }, @@ -354,6 +341,7 @@ pub struct HttpConfig { /// List of trusted reverse proxies that can set the `X-Forwarded-For` /// header #[serde(default = "default_trusted_proxies")] + #[schemars(with = "Vec", inner(ip))] pub trusted_proxies: Vec, /// Public URL base from where the authentication service is reachable diff --git a/crates/config/src/sections/matrix.rs b/crates/config/src/sections/matrix.rs index e2330a7a5..1b08c1a07 100644 --- a/crates/config/src/sections/matrix.rs +++ b/crates/config/src/sections/matrix.rs @@ -131,7 +131,11 @@ impl MatrixConfig { /// Returns an error when the shared secret could not be read from file. pub async fn secret(&self) -> anyhow::Result { Ok(match &self.secret { - Secret::File(path) => tokio::fs::read_to_string(path).await?, + Secret::File(path) => { + let raw = tokio::fs::read_to_string(path).await?; + // Trim the secret when read from file to match Synapse's behaviour + raw.trim().to_string() + } Secret::Value(secret) => secret.clone(), }) } diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index f992d8698..eb4ff2a44 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.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 anyhow::bail; +use camino::Utf8PathBuf; use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -303,3 +305,82 @@ impl ConfigurationSection for SyncConfig { Ok(()) } } + +/// Client secret config option. +/// +/// It either holds the client secret value directly or references a file where +/// the client secret is stored. +#[derive(Clone, Debug)] +pub enum ClientSecret { + /// Path to the file containing the client secret. + File(Utf8PathBuf), + + /// Client secret value. + Value(String), +} + +/// Client secret fields as serialized in JSON. +#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] +pub struct ClientSecretRaw { + /// Path to the file containing the client secret. The client secret is used + /// by the `client_secret_basic`, `client_secret_post` and + /// `client_secret_jwt` authentication methods. + #[schemars(with = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + client_secret_file: Option, + + /// Alternative to `client_secret_file`: Reads the client secret directly + /// from the config. + #[serde(skip_serializing_if = "Option::is_none")] + client_secret: Option, +} + +impl ClientSecret { + /// Returns the client secret. + /// + /// If `client_secret_file` was given, the secret is read from that file. + /// + /// # Errors + /// + /// Returns an error when the client secret could not be read from file. + pub async fn value(&self) -> anyhow::Result { + Ok(match self { + ClientSecret::File(path) => tokio::fs::read_to_string(path).await?, + ClientSecret::Value(client_secret) => client_secret.clone(), + }) + } +} + +impl TryFrom for Option { + type Error = anyhow::Error; + + fn try_from(value: ClientSecretRaw) -> Result { + match (value.client_secret, value.client_secret_file) { + (None, None) => Ok(None), + (None, Some(path)) => Ok(Some(ClientSecret::File(path))), + (Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))), + (Some(_), Some(_)) => { + bail!("Cannot specify both `client_secret` and `client_secret_file`") + } + } + } +} + +impl From> for ClientSecretRaw { + fn from(value: Option) -> Self { + match value { + Some(ClientSecret::File(path)) => ClientSecretRaw { + client_secret_file: Some(path), + client_secret: None, + }, + Some(ClientSecret::Value(client_secret)) => ClientSecretRaw { + client_secret_file: None, + client_secret: Some(client_secret), + }, + None => ClientSecretRaw { + client_secret_file: None, + client_secret: None, + }, + } + } +} diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index ac93a7abc..8583e776f 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -20,10 +20,6 @@ use tracing::info; use super::ConfigurationSection; -fn example_secret() -> &'static str { - "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" -} - /// Password config option. /// /// It either holds the password value directly or references a file where the @@ -209,7 +205,7 @@ struct EncryptionRaw { #[schemars( with = "Option", regex(pattern = r"[0-9a-fA-F]{64}"), - example = "example_secret" + example = &"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" )] #[serde_as(as = "Option")] #[serde(skip_serializing_if = "Option::is_none")] @@ -534,7 +530,10 @@ mod tests { keys_dir: keys "}, )?; - jail.create_file("encryption", example_secret())?; + jail.create_file( + "encryption", + "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff", + )?; jail.create_dir("keys")?; jail.create_file( "keys/key1", diff --git a/crates/config/src/sections/telemetry.rs b/crates/config/src/sections/telemetry.rs index 44e75a354..9d9308fd9 100644 --- a/crates/config/src/sections/telemetry.rs +++ b/crates/config/src/sections/telemetry.rs @@ -11,10 +11,6 @@ use url::Url; use super::ConfigurationSection; -fn sample_rate_example() -> f64 { - 0.5 -} - /// Propagation format for incoming and outgoing requests #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] @@ -70,7 +66,7 @@ pub struct TracingConfig { /// /// Defaults to `1.0` if not set. #[serde(skip_serializing_if = "Option::is_none")] - #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))] + #[schemars(example = 0.5, range(min = 0.0, max = 1.0))] pub sample_rate: Option, } @@ -123,26 +119,18 @@ impl MetricsConfig { } } -fn sentry_dsn_example() -> &'static str { - "https://public@host:port/1" -} - -fn sentry_environment_example() -> &'static str { - "production" -} - /// Configuration related to the Sentry integration #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct SentryConfig { /// Sentry DSN - #[schemars(url, example = "sentry_dsn_example")] + #[schemars(url, example = &"https://public@host:port/1")] #[serde(skip_serializing_if = "Option::is_none")] pub dsn: Option, /// Environment to use when sending events to Sentry /// /// Defaults to `production` if not set. - #[schemars(example = "sentry_environment_example")] + #[schemars(example = &"production")] #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, @@ -150,14 +138,14 @@ pub struct SentryConfig { /// /// Defaults to `1.0` if not set. #[serde(skip_serializing_if = "Option::is_none")] - #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))] + #[schemars(example = 0.5, range(min = 0.0, max = 1.0))] pub sample_rate: Option, /// Sample rate for tracing transactions /// /// Defaults to `0.0` if not set. #[serde(skip_serializing_if = "Option::is_none")] - #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))] + #[schemars(example = 0.5, range(min = 0.0, max = 1.0))] pub traces_sample_rate: Option, } diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index 05f70cc67..53eae7a1b 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -10,11 +10,11 @@ use camino::Utf8PathBuf; use mas_iana::jose::JsonWebSignatureAlg; use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::Error}; -use serde_with::skip_serializing_none; +use serde_with::{serde_as, skip_serializing_none}; use ulid::Ulid; use url::Url; -use crate::ConfigurationSection; +use crate::{ClientSecret, ClientSecretRaw, ConfigurationSection}; /// Upstream OAuth 2.0 providers configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] @@ -475,6 +475,7 @@ impl OnBackchannelLogout { } /// Configuration for one upstream OAuth 2 provider. +#[serde_as] #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct Provider { @@ -541,8 +542,10 @@ pub struct Provider { /// /// Used by the `client_secret_basic`, `client_secret_post`, and /// `client_secret_jwt` methods - #[serde(skip_serializing_if = "Option::is_none")] - pub client_secret: Option, + #[schemars(with = "ClientSecretRaw")] + #[serde_as(as = "serde_with::TryFromInto")] + #[serde(flatten)] + pub client_secret: Option, /// The method to authenticate the client with the provider pub token_endpoint_auth_method: TokenAuthMethod, @@ -656,3 +659,110 @@ pub struct Provider { #[serde(default, skip_serializing_if = "OnBackchannelLogout::is_default")] pub on_backchannel_logout: OnBackchannelLogout, } + +impl Provider { + /// Returns the client secret. + /// + /// If `client_secret_file` was given, the secret is read from that file. + /// + /// # Errors + /// + /// Returns an error when the client secret could not be read from file. + pub async fn client_secret(&self) -> anyhow::Result> { + Ok(match &self.client_secret { + Some(client_secret) => Some(client_secret.value().await?), + None => None, + }) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use figment::{ + Figment, Jail, + providers::{Format, Yaml}, + }; + use tokio::{runtime::Handle, task}; + + use super::*; + + #[tokio::test] + async fn load_config() { + task::spawn_blocking(|| { + Jail::expect_with(|jail| { + jail.create_file( + "config.yaml", + r#" + upstream_oauth2: + providers: + - id: 01GFWR28C4KNE04WG3HKXB7C9R + client_id: upstream-oauth2 + token_endpoint_auth_method: none + + - id: 01GFWR32NCQ12B8Z0J8CPXRRB6 + client_id: upstream-oauth2 + client_secret_file: secret + token_endpoint_auth_method: client_secret_basic + + - id: 01GFWR3WHR93Y5HK389H28VHZ9 + client_id: upstream-oauth2 + client_secret: c1!3n753c237 + token_endpoint_auth_method: client_secret_post + + - id: 01GFWR43R2ZZ8HX9CVBNW9TJWG + client_id: upstream-oauth2 + client_secret_file: secret + token_endpoint_auth_method: client_secret_jwt + + - id: 01GFWR4BNFDCC4QDG6AMSP1VRR + client_id: upstream-oauth2 + token_endpoint_auth_method: private_key_jwt + jwks: + keys: + - kid: "03e84aed4ef4431014e8617567864c4efaaaede9" + kty: "RSA" + alg: "RS256" + use: "sig" + e: "AQAB" + n: "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw" + + - kid: "d01c1abe249269f72ef7ca2613a86c9f05e59567" + kty: "RSA" + alg: "RS256" + use: "sig" + e: "AQAB" + n: "0hukqytPwrj1RbMYhYoepCi3CN5k7DwYkTe_Cmb7cP9_qv4ok78KdvFXt5AnQxCRwBD7-qTNkkfMWO2RxUMBdQD0ED6tsSb1n5dp0XY8dSWiBDCX8f6Hr-KolOpvMLZKRy01HdAWcM6RoL9ikbjYHUEW1C8IJnw3MzVHkpKFDL354aptdNLaAdTCBvKzU9WpXo10g-5ctzSlWWjQuecLMQ4G1mNdsR1LHhUENEnOvgT8cDkX0fJzLbEbyBYkdMgKggyVPEB1bg6evG4fTKawgnf0IDSPxIU-wdS9wdSP9ZCJJPLi5CEp-6t6rE_sb2dGcnzjCGlembC57VwpkUvyMw" + "#, + )?; + jail.create_file("secret", r"c1!3n753c237")?; + + let config = Figment::new() + .merge(Yaml::file("config.yaml")) + .extract_inner::("upstream_oauth2")?; + + assert_eq!(config.providers.len(), 5); + + assert_eq!( + config.providers[1].id, + Ulid::from_str("01GFWR32NCQ12B8Z0J8CPXRRB6").unwrap() + ); + + assert!(config.providers[0].client_secret.is_none()); + assert!(matches!(config.providers[1].client_secret, Some(ClientSecret::File(ref p)) if p == "secret")); + assert!(matches!(config.providers[2].client_secret, Some(ClientSecret::Value(ref v)) if v == "c1!3n753c237")); + assert!(matches!(config.providers[3].client_secret, Some(ClientSecret::File(ref p)) if p == "secret")); + assert!(config.providers[4].client_secret.is_none()); + + Handle::current().block_on(async move { + assert_eq!(config.providers[1].client_secret().await.unwrap().unwrap(), "c1!3n753c237"); + assert_eq!(config.providers[2].client_secret().await.unwrap().unwrap(), "c1!3n753c237"); + assert_eq!(config.providers[3].client_secret().await.unwrap().unwrap(), "c1!3n753c237"); + }); + + Ok(()) + }); + }).await.unwrap(); + } +} diff --git a/crates/context/src/fmt.rs b/crates/context/src/fmt.rs index 25908a2ca..47e72fcae 100644 --- a/crates/context/src/fmt.rs +++ b/crates/context/src/fmt.rs @@ -4,10 +4,7 @@ // Please see LICENSE files in the repository root for full details. use console::{Color, Style}; -use opentelemetry::{ - TraceId, - trace::{SamplingDecision, TraceContextExt}, -}; +use opentelemetry::TraceId; use tracing::{Level, Subscriber}; use tracing_opentelemetry::OtelData; use tracing_subscriber::{ @@ -21,7 +18,7 @@ use tracing_subscriber::{ use crate::LogContext; -/// An event formatter usable by the [`tracing-subscriber`] crate, which +/// An event formatter usable by the [`tracing_subscriber`] crate, which /// includes the log context and the OTEL trace ID. #[derive(Debug, Default)] pub struct EventFormatter; @@ -131,31 +128,14 @@ where // If we have a OTEL span, we can add the trace ID to the end of the log line if let Some(span) = ctx.lookup_current() && let Some(otel) = span.extensions().get::() + && let Some(trace_id) = otel.trace_id() + && trace_id != TraceId::INVALID { - let parent_cx_span = otel.parent_cx.span(); - let sc = parent_cx_span.span_context(); - - // Check if the span is sampled, first from the span builder, - // then from the parent context if nothing is set there - if otel - .builder - .sampling_result - .as_ref() - .map_or(sc.is_sampled(), |r| { - r.decision == SamplingDecision::RecordAndSample - }) - { - // If it is the root span, the trace ID will be in the span builder. Else, it - // will be in the parent OTEL context - let trace_id = otel.builder.trace_id.unwrap_or(sc.trace_id()); - if trace_id != TraceId::INVALID { - let label = Style::new() - .italic() - .force_styling(ansi) - .apply_to("trace.id"); - write!(&mut writer, " {label}={trace_id}")?; - } - } + let label = Style::new() + .italic() + .force_styling(ansi) + .apply_to("trace.id"); + write!(&mut writer, " {label}={trace_id}")?; } writeln!(&mut writer) diff --git a/crates/data-model/src/lib.rs b/crates/data-model/src/lib.rs index 6be06b4d9..962c8be00 100644 --- a/crates/data-model/src/lib.rs +++ b/crates/data-model/src/lib.rs @@ -11,6 +11,7 @@ use thiserror::Error; pub mod clock; pub(crate) mod compat; pub mod oauth2; +pub mod personal; pub(crate) mod policy_data; mod site_config; pub(crate) mod tokens; @@ -18,6 +19,7 @@ pub(crate) mod upstream_oauth2; pub(crate) mod user_agent; pub(crate) mod users; mod utils; +mod version; /// Error when an invalid state transition is attempted. #[derive(Debug, Error)] @@ -57,4 +59,5 @@ pub use self::{ UserRecoveryTicket, UserRegistration, UserRegistrationPassword, UserRegistrationToken, }, utils::{BoxClock, BoxRng}, + version::AppVersion, }; diff --git a/crates/data-model/src/personal/mod.rs b/crates/data-model/src/personal/mod.rs new file mode 100644 index 000000000..1142fea76 --- /dev/null +++ b/crates/data-model/src/personal/mod.rs @@ -0,0 +1,32 @@ +// 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. + +pub mod session; + +use chrono::{DateTime, Utc}; +use ulid::Ulid; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PersonalAccessToken { + pub id: Ulid, + pub session_id: Ulid, + pub created_at: DateTime, + pub expires_at: Option>, + pub revoked_at: Option>, +} + +impl PersonalAccessToken { + #[must_use] + pub fn is_valid(&self, now: DateTime) -> bool { + if self.revoked_at.is_some() { + return false; + } + if let Some(expires_at) = self.expires_at { + expires_at > now + } else { + true + } + } +} diff --git a/crates/data-model/src/personal/session.rs b/crates/data-model/src/personal/session.rs new file mode 100644 index 000000000..f3c8d34f9 --- /dev/null +++ b/crates/data-model/src/personal/session.rs @@ -0,0 +1,141 @@ +// 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. + +use std::net::IpAddr; + +use chrono::{DateTime, Utc}; +use oauth2_types::scope::Scope; +use serde::Serialize; +use ulid::Ulid; + +use crate::{Client, Device, InvalidTransitionError, User}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +pub enum SessionState { + #[default] + Valid, + Revoked { + revoked_at: DateTime, + }, +} + +impl SessionState { + /// Returns `true` if the session state is [`Valid`]. + /// + /// [`Valid`]: SessionState::Valid + #[must_use] + pub fn is_valid(&self) -> bool { + matches!(self, Self::Valid) + } + + /// Returns `true` if the session state is [`Revoked`]. + /// + /// [`Revoked`]: SessionState::Revoked + #[must_use] + pub fn is_revoked(&self) -> bool { + matches!(self, Self::Revoked { .. }) + } + + /// Transitions the session state to [`Revoked`]. + /// + /// # Parameters + /// + /// * `revoked_at` - The time at which the session was revoked. + /// + /// # Errors + /// + /// Returns an error if the session state is already [`Revoked`]. + /// + /// [`Revoked`]: SessionState::Revoked + pub fn revoke(self, revoked_at: DateTime) -> Result { + match self { + Self::Valid => Ok(Self::Revoked { revoked_at }), + Self::Revoked { .. } => Err(InvalidTransitionError), + } + } + + /// Returns the time the session was revoked, if any + /// + /// Returns `None` if the session is still [`Valid`]. + /// + /// [`Valid`]: SessionState::Valid + #[must_use] + pub fn revoked_at(&self) -> Option> { + match self { + Self::Valid => None, + Self::Revoked { revoked_at } => Some(*revoked_at), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct PersonalSession { + pub id: Ulid, + pub state: SessionState, + pub owner: PersonalSessionOwner, + pub actor_user_id: Ulid, + pub human_name: String, + /// The scope for the session, identical to OAuth 2 sessions. + /// May or may not include a device scope + /// (personal sessions can be deviceless). + pub scope: Scope, + pub created_at: DateTime, + pub last_active_at: Option>, + pub last_active_ip: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] +pub enum PersonalSessionOwner { + /// The personal session is owned by the user with the given `user_id`. + User(Ulid), + /// The personal session is owned by the OAuth 2 Client with the given + /// `oauth2_client_id`. + OAuth2Client(Ulid), +} + +impl<'a> From<&'a User> for PersonalSessionOwner { + fn from(value: &'a User) -> Self { + PersonalSessionOwner::User(value.id) + } +} + +impl<'a> From<&'a Client> for PersonalSessionOwner { + fn from(value: &'a Client) -> Self { + PersonalSessionOwner::OAuth2Client(value.id) + } +} + +impl std::ops::Deref for PersonalSession { + type Target = SessionState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +impl PersonalSession { + /// Marks the session as revoked. + /// + /// # Parameters + /// + /// * `revoked_at` - The time at which the session was finished. + /// + /// # Errors + /// + /// Returns an error if the session is already finished. + pub fn finish(mut self, revoked_at: DateTime) -> Result { + self.state = self.state.revoke(revoked_at)?; + Ok(self) + } + + /// Returns whether the scope of this session contains a device scope; + /// in other words: whether this session has a device. + #[must_use] + pub fn has_device(&self) -> bool { + self.scope + .iter() + .any(|scope_token| Device::from_scope_token(scope_token).is_some()) + } +} diff --git a/crates/data-model/src/site_config.rs b/crates/data-model/src/site_config.rs index ac0d7e6b8..9622203ad 100644 --- a/crates/data-model/src/site_config.rs +++ b/crates/data-model/src/site_config.rs @@ -64,6 +64,9 @@ pub struct SiteConfig { /// Whether password registration is enabled. pub password_registration_enabled: bool, + /// Whether a valid email address is required for password registrations. + pub password_registration_email_required: bool, + /// Whether registration tokens are required for password registrations. pub registration_token_required: bool, diff --git a/crates/data-model/src/tokens.rs b/crates/data-model/src/tokens.rs index 1ea5be6be..bd34c5000 100644 --- a/crates/data-model/src/tokens.rs +++ b/crates/data-model/src/tokens.rs @@ -240,6 +240,9 @@ pub enum TokenType { /// A legacy refresh token CompatRefreshToken, + + /// A personal access token. + PersonalAccessToken, } impl std::fmt::Display for TokenType { @@ -249,6 +252,7 @@ impl std::fmt::Display for TokenType { TokenType::RefreshToken => write!(f, "refresh token"), TokenType::CompatAccessToken => write!(f, "compat access token"), TokenType::CompatRefreshToken => write!(f, "compat refresh token"), + TokenType::PersonalAccessToken => write!(f, "personal access token"), } } } @@ -260,6 +264,7 @@ impl TokenType { TokenType::RefreshToken => "mar", TokenType::CompatAccessToken => "mct", TokenType::CompatRefreshToken => "mcr", + TokenType::PersonalAccessToken => "mpt", } } @@ -269,6 +274,7 @@ impl TokenType { "mar" => Some(TokenType::RefreshToken), "mct" | "syt" => Some(TokenType::CompatAccessToken), "mcr" | "syr" => Some(TokenType::CompatRefreshToken), + "mpt" => Some(TokenType::PersonalAccessToken), _ => None, } } @@ -335,7 +341,9 @@ impl PartialEq for TokenType { matches!( (self, other), ( - TokenType::AccessToken | TokenType::CompatAccessToken, + TokenType::AccessToken + | TokenType::CompatAccessToken + | TokenType::PersonalAccessToken, OAuthTokenTypeHint::AccessToken ) | ( TokenType::RefreshToken | TokenType::CompatRefreshToken, diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index 920726ef8..7c7da6293 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -21,6 +21,7 @@ pub struct User { pub locked_at: Option>, pub deactivated_at: Option>, pub can_request_admin: bool, + pub is_guest: bool, } impl User { @@ -29,6 +30,20 @@ impl User { pub fn is_valid(&self) -> bool { self.locked_at.is_none() && self.deactivated_at.is_none() } + + /// Returns `true` if the user is a valid actor, for example + /// of a personal session. + /// + /// Currently: this is `true` unless the user is deactivated. + /// + /// This is a weaker form of validity: `is_valid` always implies + /// `is_valid_actor`, but some users (currently: locked users) + /// can be valid actors for personal sessions but aren't valid + /// except through administrative access. + #[must_use] + pub fn is_valid_actor(&self) -> bool { + self.deactivated_at.is_none() + } } impl User { @@ -43,6 +58,7 @@ impl User { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: false, }] } } diff --git a/crates/data-model/src/version.rs b/crates/data-model/src/version.rs new file mode 100644 index 000000000..86d890fc1 --- /dev/null +++ b/crates/data-model/src/version.rs @@ -0,0 +1,8 @@ +// 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. + +/// A structure which holds information about the running version of the app +#[derive(Debug, Clone, Copy)] +pub struct AppVersion(pub &'static str); diff --git a/crates/email/src/transport.rs b/crates/email/src/transport.rs index 9161d76e5..004844ab1 100644 --- a/crates/email/src/transport.rs +++ b/crates/email/src/transport.rs @@ -36,7 +36,9 @@ pub struct Transport { inner: Arc, } +#[derive(Default)] enum TransportInner { + #[default] Blackhole, Smtp(AsyncSmtpTransport), Sendmail(AsyncSendmailTransport), @@ -113,12 +115,6 @@ impl Transport { } } -impl Default for TransportInner { - fn default() -> Self { - Self::Blackhole - } -} - #[derive(Debug, Error)] #[error(transparent)] pub enum Error { diff --git a/crates/handlers/src/activity_tracker/bound.rs b/crates/handlers/src/activity_tracker/bound.rs index 14d36fb7c..8f7acbdde 100644 --- a/crates/handlers/src/activity_tracker/bound.rs +++ b/crates/handlers/src/activity_tracker/bound.rs @@ -6,7 +6,9 @@ use std::net::IpAddr; -use mas_data_model::{BrowserSession, Clock, CompatSession, Session}; +use mas_data_model::{ + BrowserSession, Clock, CompatSession, Session, personal::session::PersonalSession, +}; use crate::activity_tracker::ActivityTracker; @@ -37,6 +39,13 @@ impl Bound { .await; } + /// Record activity in a personal session. + pub async fn record_personal_session(&self, clock: &dyn Clock, session: &PersonalSession) { + self.tracker + .record_personal_session(clock, session, self.ip) + .await; + } + /// Record activity in a compatibility session. pub async fn record_compat_session(&self, clock: &dyn Clock, session: &CompatSession) { self.tracker diff --git a/crates/handlers/src/activity_tracker/mod.rs b/crates/handlers/src/activity_tracker/mod.rs index 738da3856..e1c6b976f 100644 --- a/crates/handlers/src/activity_tracker/mod.rs +++ b/crates/handlers/src/activity_tracker/mod.rs @@ -10,7 +10,9 @@ mod worker; use std::net::IpAddr; use chrono::{DateTime, Utc}; -use mas_data_model::{BrowserSession, Clock, CompatSession, Session}; +use mas_data_model::{ + BrowserSession, Clock, CompatSession, Session, personal::session::PersonalSession, +}; use mas_storage::BoxRepositoryFactory; use tokio_util::{sync::CancellationToken, task::TaskTracker}; use ulid::Ulid; @@ -24,6 +26,8 @@ static MESSAGE_QUEUE_SIZE: usize = 1000; enum SessionKind { OAuth2, Compat, + /// Session associated with personal access tokens + Personal, Browser, } @@ -32,6 +36,7 @@ impl SessionKind { match self { SessionKind::OAuth2 => "oauth2", SessionKind::Compat => "compat", + SessionKind::Personal => "personal", SessionKind::Browser => "browser", } } @@ -108,6 +113,28 @@ impl ActivityTracker { } } + /// Record activity in a personal session. + pub async fn record_personal_session( + &self, + clock: &dyn Clock, + session: &PersonalSession, + ip: Option, + ) { + let res = self + .channel + .send(Message::Record { + kind: SessionKind::Personal, + id: session.id, + date_time: clock.now(), + ip, + }) + .await; + + if let Err(e) = res { + tracing::error!("Failed to record Personal session: {}", e); + } + } + /// Record activity in a compat session. pub async fn record_compat_session( &self, diff --git a/crates/handlers/src/activity_tracker/worker.rs b/crates/handlers/src/activity_tracker/worker.rs index 46cc84ccd..9405eab41 100644 --- a/crates/handlers/src/activity_tracker/worker.rs +++ b/crates/handlers/src/activity_tracker/worker.rs @@ -224,6 +224,7 @@ impl Worker { let mut browser_sessions = Vec::new(); let mut oauth2_sessions = Vec::new(); let mut compat_sessions = Vec::new(); + let mut personal_sessions = Vec::new(); for ((kind, id), record) in pending_records { match kind { @@ -236,6 +237,9 @@ impl Worker { SessionKind::Compat => { compat_sessions.push((*id, record.end_time, record.ip)); } + SessionKind::Personal => { + personal_sessions.push((*id, record.end_time, record.ip)); + } } } @@ -253,6 +257,9 @@ impl Worker { repo.compat_session() .record_batch_activity(compat_sessions) .await?; + repo.personal_session() + .record_batch_activity(personal_sessions) + .await?; repo.save().await?; self.pending_records.clear(); diff --git a/crates/handlers/src/admin/call_context.rs b/crates/handlers/src/admin/call_context.rs index ebe0e02e5..1cffe682e 100644 --- a/crates/handlers/src/admin/call_context.rs +++ b/crates/handlers/src/admin/call_context.rs @@ -16,8 +16,12 @@ use axum_extra::TypedHeader; use headers::{Authorization, authorization::Bearer}; use hyper::StatusCode; use mas_axum_utils::record_error; -use mas_data_model::{BoxClock, Session, User}; +use mas_data_model::{ + BoxClock, Session, TokenFormatError, TokenType, User, + personal::session::{PersonalSession, PersonalSessionOwner}, +}; use mas_storage::{BoxRepository, RepositoryError}; +use oauth2_types::scope::Scope; use ulid::Ulid; use super::response::ErrorResponse; @@ -41,6 +45,10 @@ pub enum Rejection { #[error("Invalid repository operation")] Repository(#[from] RepositoryError), + /// The access token was not of the correct type for the Admin API + #[error("Invalid type of access token")] + InvalidAccessTokenType(#[from] Option), + /// The access token could not be found in the database #[error("Unknown access token")] UnknownAccessToken, @@ -90,7 +98,8 @@ impl IntoResponse for Rejection { | Rejection::TokenExpired | Rejection::SessionRevoked | Rejection::UserLocked - | Rejection::MissingScope => StatusCode::UNAUTHORIZED, + | Rejection::MissingScope + | Rejection::InvalidAccessTokenType(_) => StatusCode::UNAUTHORIZED, Rejection::RepositorySetup(_) | Rejection::Repository(_) @@ -113,7 +122,7 @@ pub struct CallContext { pub repo: BoxRepository, pub clock: BoxClock, pub user: Option, - pub session: Session, + pub session: CallerSession, } impl FromRequestParts for CallContext @@ -154,56 +163,126 @@ where })?; let token = token.token(); + let token_type = TokenType::check(token)?; - // Look for the access token in the database - let token = repo - .oauth2_access_token() - .find_by_token(token) - .await? - .ok_or(Rejection::UnknownAccessToken)?; + let session = match token_type { + TokenType::AccessToken => { + // Look for the access token in the database + let token = repo + .oauth2_access_token() + .find_by_token(token) + .await? + .ok_or(Rejection::UnknownAccessToken)?; - // Look for the associated session in the database - let session = repo - .oauth2_session() - .lookup(token.session_id) - .await? - .ok_or_else(|| Rejection::LoadSession(token.session_id))?; + // Look for the associated session in the database + let session = repo + .oauth2_session() + .lookup(token.session_id) + .await? + .ok_or_else(|| Rejection::LoadSession(token.session_id))?; - // Record the activity on the session - activity_tracker - .record_oauth2_session(&clock, &session) - .await; + if !session.is_valid() { + return Err(Rejection::SessionRevoked); + } + + if !token.is_valid(clock.now()) { + return Err(Rejection::TokenExpired); + } + + // Record the activity on the session + activity_tracker + .record_oauth2_session(&clock, &session) + .await; + + CallerSession::OAuth2Session(session) + } + TokenType::PersonalAccessToken => { + // Look for the access token in the database + let token = repo + .personal_access_token() + .find_by_token(token) + .await? + .ok_or(Rejection::UnknownAccessToken)?; + + // Look for the associated session in the database + let session = repo + .personal_session() + .lookup(token.session_id) + .await? + .ok_or_else(|| Rejection::LoadSession(token.session_id))?; + + if !session.is_valid() { + return Err(Rejection::SessionRevoked); + } + + if !token.is_valid(clock.now()) { + return Err(Rejection::TokenExpired); + } + + // Check the validity of the owner of the personal session + match session.owner { + PersonalSessionOwner::User(owner_user_id) => { + let owner_user = repo + .user() + .lookup(owner_user_id) + .await? + .ok_or_else(|| Rejection::LoadUser(owner_user_id))?; + if !owner_user.is_valid() { + return Err(Rejection::UserLocked); + } + } + PersonalSessionOwner::OAuth2Client(_) => { + // nop: Client owners are always valid + } + } + + // Record the activity on the session + activity_tracker + .record_personal_session(&clock, &session) + .await; + + CallerSession::PersonalSession(session) + } + _other => { + return Err(Rejection::InvalidAccessTokenType(None)); + } + }; // Load the user if there is one - let user = if let Some(user_id) = session.user_id { + let user = if let Some(user_id) = session.user_id() { let user = repo .user() .lookup(user_id) .await? .ok_or_else(|| Rejection::LoadUser(user_id))?; + + match session { + CallerSession::OAuth2Session(_) => { + // For OAuth2 sessions: check that the user is valid enough + // to be a user. + if !user.is_valid() { + return Err(Rejection::UserLocked); + } + } + CallerSession::PersonalSession(_) => { + // For personal sessions: check that the actor is valid enough + // to be an actor. + if !user.is_valid_actor() { + return Err(Rejection::UserLocked); + } + } + } + Some(user) } else { + // Double check we're not using a PersonalSession + assert!(matches!(session, CallerSession::OAuth2Session(_))); None }; - // If there is a user for this session, check that it is not locked - if let Some(user) = &user - && !user.is_valid() - { - return Err(Rejection::UserLocked); - } - - if !session.is_valid() { - return Err(Rejection::SessionRevoked); - } - - if !token.is_valid(clock.now()) { - return Err(Rejection::TokenExpired); - } - // For now, we only check that the session has the admin scope // Later we might want to check other route-specific scopes - if !session.scope.contains("urn:mas:admin") { + if !session.scope().contains("urn:mas:admin") { return Err(Rejection::MissingScope); } @@ -215,3 +294,26 @@ where }) } } + +/// The session representing the caller of the Admin API; +/// could either be an OAuth session or a personal session. +pub enum CallerSession { + OAuth2Session(Session), + PersonalSession(PersonalSession), +} + +impl CallerSession { + pub fn scope(&self) -> &Scope { + match self { + CallerSession::OAuth2Session(session) => &session.scope, + CallerSession::PersonalSession(session) => &session.scope, + } + } + + pub fn user_id(&self) -> Option { + match self { + CallerSession::OAuth2Session(session) => session.user_id, + CallerSession::PersonalSession(session) => Some(session.actor_user_id), + } + } +} diff --git a/crates/handlers/src/admin/mod.rs b/crates/handlers/src/admin/mod.rs index 8cc2956c0..cbb23edbf 100644 --- a/crates/handlers/src/admin/mod.rs +++ b/crates/handlers/src/admin/mod.rs @@ -20,7 +20,7 @@ use axum::{ use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; use indexmap::IndexMap; use mas_axum_utils::InternalError; -use mas_data_model::BoxRng; +use mas_data_model::{AppVersion, BoxRng, SiteConfig}; use mas_http::CorsLayerExt; use mas_matrix::HomeserverConnection; use mas_policy::PolicyFactory; @@ -29,6 +29,7 @@ use mas_router::{ UrlBuilder, }; use mas_templates::{ApiDocContext, Templates}; +use schemars::transform::AddNullable; use tower_http::cors::{Any, CorsLayer}; mod call_context; @@ -43,6 +44,11 @@ use crate::passwords::PasswordManager; fn finish(t: TransformOpenApi) -> TransformOpenApi { t.title("Matrix Authentication Service admin API") + .tag(Tag { + name: "server".to_owned(), + description: Some("Information about the server".to_owned()), + ..Tag::default() + }) .tag(Tag { name: "compat-session".to_owned(), description: Some("Manage compatibility sessions from legacy clients".to_owned()), @@ -86,6 +92,11 @@ fn finish(t: TransformOpenApi) -> TransformOpenApi { ), ..Default::default() }) + .tag(Tag { + name: "upstream-oauth-provider".to_owned(), + description: Some("Manage upstream OAuth 2.0 providers".to_owned()), + ..Tag::default() + }) .security_scheme("oauth2", oauth_security_scheme(None)) .security_scheme( "token", @@ -153,14 +164,24 @@ where Templates: FromRef, UrlBuilder: FromRef, Arc: FromRef, + SiteConfig: FromRef, + AppVersion: FromRef, { // We *always* want to explicitly set the possible responses, beacuse the // infered ones are not necessarily correct aide::generate::infer_responses(false); aide::generate::in_context(|ctx| { - ctx.schema = - schemars::r#gen::SchemaGenerator::new(schemars::r#gen::SchemaSettings::openapi3()); + ctx.schema = schemars::generate::SchemaGenerator::new( + schemars::generate::SchemaSettings::openapi3().with(|settings| { + // Remove the transform which adds nullable fields, as it's not + // valid with OpenAPI 3.1. For some reason, aide/schemars output + // an OpenAPI 3.1 schema with this nullable transform. + settings + .transforms + .retain(|transform| !transform.is::()); + }), + ); }); let mut api = OpenApi::default(); diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs index 62065dfe4..7936c02f8 100644 --- a/crates/handlers/src/admin/model.rs +++ b/crates/handlers/src/admin/model.rs @@ -7,9 +7,16 @@ use std::net::IpAddr; use chrono::{DateTime, Utc}; -use mas_data_model::Device; +use mas_data_model::{ + Device, + personal::{ + PersonalAccessToken as DataModelPersonalAccessToken, + session::{PersonalSession as DataModelPersonalSession, PersonalSessionOwner}, + }, +}; use schemars::JsonSchema; use serde::Serialize; +use thiserror::Error; use ulid::Ulid; use url::Url; @@ -52,6 +59,9 @@ pub struct User { /// Whether the user can request admin privileges. admin: bool, + + /// Whether the user was a guest before migrating to MAS, + legacy_guest: bool, } impl User { @@ -65,6 +75,7 @@ impl User { locked_at: None, deactivated_at: None, admin: false, + legacy_guest: false, }, Self { id: Ulid::from_bytes([0x02; 16]), @@ -73,6 +84,7 @@ impl User { locked_at: None, deactivated_at: None, admin: true, + legacy_guest: false, }, Self { id: Ulid::from_bytes([0x03; 16]), @@ -81,6 +93,7 @@ impl User { locked_at: Some(DateTime::default()), deactivated_at: None, admin: false, + legacy_guest: true, }, ] } @@ -95,6 +108,7 @@ impl From for User { locked_at: user.locked_at, deactivated_at: user.deactivated_at, admin: user.can_request_admin, + legacy_guest: user.is_guest, } } } @@ -688,3 +702,255 @@ impl UserRegistrationToken { ] } } + +/// An upstream OAuth 2.0 provider +#[derive(Serialize, JsonSchema)] +pub struct UpstreamOAuthProvider { + #[serde(skip)] + id: Ulid, + + /// The OIDC issuer of the provider + issuer: Option, + + /// A human-readable name for the provider + human_name: Option, + + /// A brand identifier, e.g. "apple" or "google" + brand_name: Option, + + /// When the provider was created + created_at: DateTime, + + /// When the provider was disabled. If null, the provider is enabled. + disabled_at: Option>, +} + +impl From for UpstreamOAuthProvider { + fn from(provider: mas_data_model::UpstreamOAuthProvider) -> Self { + Self { + id: provider.id, + issuer: provider.issuer, + human_name: provider.human_name, + brand_name: provider.brand_name, + created_at: provider.created_at, + disabled_at: provider.disabled_at, + } + } +} + +impl Resource for UpstreamOAuthProvider { + const KIND: &'static str = "upstream-oauth-provider"; + const PATH: &'static str = "/api/admin/v1/upstream-oauth-providers"; + + fn id(&self) -> Ulid { + self.id + } +} + +impl UpstreamOAuthProvider { + /// Samples of upstream OAuth 2.0 providers + pub fn samples() -> [Self; 3] { + [ + Self { + id: Ulid::from_bytes([0x01; 16]), + issuer: Some("https://accounts.google.com".to_owned()), + human_name: Some("Google".to_owned()), + brand_name: Some("google".to_owned()), + created_at: DateTime::default(), + disabled_at: None, + }, + Self { + id: Ulid::from_bytes([0x02; 16]), + issuer: Some("https://appleid.apple.com".to_owned()), + human_name: Some("Apple ID".to_owned()), + brand_name: Some("apple".to_owned()), + created_at: DateTime::default(), + disabled_at: Some(DateTime::default()), + }, + Self { + id: Ulid::from_bytes([0x03; 16]), + issuer: None, + human_name: Some("Custom OAuth Provider".to_owned()), + brand_name: None, + created_at: DateTime::default(), + disabled_at: None, + }, + ] + } +} + +/// An error that shouldn't happen in practice, but suggests database +/// inconsistency. +#[derive(Debug, Error)] +#[error( + "personal session {session_id} in inconsistent state: not revoked but no valid access token" +)] +pub struct InconsistentPersonalSession { + pub session_id: Ulid, +} + +// Note: we don't expose a separate concept of personal access tokens to the +// admin API; we merge the relevant attributes into the personal session. +/// A personal session (session using personal access tokens) +#[derive(Serialize, JsonSchema)] +pub struct PersonalSession { + #[serde(skip)] + id: Ulid, + + /// When the session was created + created_at: DateTime, + + /// When the session was revoked, if applicable + revoked_at: Option>, + + /// The ID of the user who owns this session (if user-owned) + #[schemars(with = "Option")] + owner_user_id: Option, + + /// The ID of the `OAuth2` client that owns this session (if client-owned) + #[schemars(with = "Option")] + owner_client_id: Option, + + /// The ID of the user that the session acts on behalf of + #[schemars(with = "super::schema::Ulid")] + actor_user_id: Ulid, + + /// Human-readable name for the session + human_name: String, + + /// `OAuth2` scopes for this session + scope: String, + + /// When the session was last active + last_active_at: Option>, + + /// IP address of last activity + last_active_ip: Option, + + /// When the current token for this session expires. + /// The session will need to be regenerated, producing a new access token, + /// after this time. + /// None if the current token won't expire or if the session is revoked. + expires_at: Option>, + + /// The actual access token (only returned on creation) + #[serde(skip_serializing_if = "Option::is_none")] + access_token: Option, +} + +impl + TryFrom<( + DataModelPersonalSession, + Option, + )> for PersonalSession +{ + type Error = InconsistentPersonalSession; + + fn try_from( + (session, token): ( + DataModelPersonalSession, + Option, + ), + ) -> Result { + let expires_at = if let Some(token) = token { + token.expires_at + } else { + if !session.is_revoked() { + // No active token, but the session is not revoked. + return Err(InconsistentPersonalSession { + session_id: session.id, + }); + } + None + }; + + let (owner_user_id, owner_client_id) = match session.owner { + PersonalSessionOwner::User(id) => (Some(id), None), + PersonalSessionOwner::OAuth2Client(id) => (None, Some(id)), + }; + + Ok(Self { + id: session.id, + created_at: session.created_at, + revoked_at: session.revoked_at(), + owner_user_id, + owner_client_id, + actor_user_id: session.actor_user_id, + human_name: session.human_name, + scope: session.scope.to_string(), + last_active_at: session.last_active_at, + last_active_ip: session.last_active_ip, + expires_at, + // If relevant, the caller will populate using `with_token` afterwards. + access_token: None, + }) + } +} + +impl Resource for PersonalSession { + const KIND: &'static str = "personal-session"; + const PATH: &'static str = "/api/admin/v1/personal-sessions"; + + fn id(&self) -> Ulid { + self.id + } +} + +impl PersonalSession { + /// Sample personal sessions for documentation/testing + pub fn samples() -> [Self; 3] { + [ + Self { + id: Ulid::from_string("01FSHN9AG0AJ6AC5HQ9X6H4RP4").unwrap(), + created_at: DateTime::from_timestamp(1_642_338_000, 0).unwrap(), /* 2022-01-16T14: + * 40:00Z */ + revoked_at: None, + owner_user_id: Some(Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap()), + owner_client_id: None, + actor_user_id: Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap(), + human_name: "Alice's Development Token".to_owned(), + scope: "openid urn:matrix:org.matrix.msc2967.client:api:*".to_owned(), + last_active_at: Some(DateTime::from_timestamp(1_642_347_000, 0).unwrap()), /* 2022-01-16T17:10:00Z */ + last_active_ip: Some("192.168.1.100".parse().unwrap()), + expires_at: None, + access_token: None, + }, + Self { + id: Ulid::from_string("01FSHN9AG0BJ6AC5HQ9X6H4RP5").unwrap(), + created_at: DateTime::from_timestamp(1_642_338_060, 0).unwrap(), /* 2022-01-16T14: + * 41:00Z */ + revoked_at: Some(DateTime::from_timestamp(1_642_350_000, 0).unwrap()), /* 2022-01-16T18:00:00Z */ + owner_user_id: Some(Ulid::from_string("01FSHN9AG0NZAA6S4AF7CTV32F").unwrap()), + owner_client_id: None, + actor_user_id: Ulid::from_string("01FSHN9AG0NZAA6S4AF7CTV32F").unwrap(), + human_name: "Bob's Mobile App".to_owned(), + scope: "openid".to_owned(), + last_active_at: Some(DateTime::from_timestamp(1_642_349_000, 0).unwrap()), /* 2022-01-16T17:43:20Z */ + last_active_ip: Some("10.0.0.50".parse().unwrap()), + expires_at: None, + access_token: None, + }, + Self { + id: Ulid::from_string("01FSHN9AG0CJ6AC5HQ9X6H4RP6").unwrap(), + created_at: DateTime::from_timestamp(1_642_338_120, 0).unwrap(), /* 2022-01-16T14: + * 42:00Z */ + revoked_at: None, + owner_user_id: None, + owner_client_id: Some(Ulid::from_string("01FSHN9AG0DJ6AC5HQ9X6H4RP7").unwrap()), + actor_user_id: Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap(), + human_name: "CI/CD Pipeline Token".to_owned(), + scope: "openid urn:mas:admin".to_owned(), + last_active_at: Some(DateTime::from_timestamp(1_642_348_000, 0).unwrap()), /* 2022-01-16T17:26:40Z */ + last_active_ip: Some("203.0.113.10".parse().unwrap()), + expires_at: Some(DateTime::from_timestamp(1_642_999_000, 0).unwrap()), + access_token: None, + }, + ] + } + + /// Add the actual token value (for use in creation responses) + pub fn with_token(mut self, access_token: String) -> Self { + self.access_token = Some(access_token); + self + } +} diff --git a/crates/handlers/src/admin/params.rs b/crates/handlers/src/admin/params.rs index 633917d9a..4b1ccb1de 100644 --- a/crates/handlers/src/admin/params.rs +++ b/crates/handlers/src/admin/params.rs @@ -7,17 +7,15 @@ // Generated code from schemars violates this rule #![allow(clippy::str_to_string)] -use std::num::NonZeroUsize; +use std::{borrow::Cow, num::NonZeroUsize}; use aide::OperationIo; use axum::{ Json, - extract::{ - FromRequestParts, Path, Query, - rejection::{PathRejection, QueryRejection}, - }, + extract::{FromRequestParts, Path, rejection::PathRejection}, response::IntoResponse, }; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_storage::pagination::PaginationDirection; @@ -64,6 +62,34 @@ impl std::ops::Deref for UlidPathParam { /// The default page size if not specified const DEFAULT_PAGE_SIZE: usize = 10; +#[derive(Deserialize, JsonSchema, Clone, Copy, Default, Debug)] +pub enum IncludeCount { + /// Include the total number of items (default) + #[default] + #[serde(rename = "true")] + True, + + /// Do not include the total number of items + #[serde(rename = "false")] + False, + + /// Only include the total number of items, skip the items themselves + #[serde(rename = "only")] + Only, +} + +impl IncludeCount { + pub(crate) fn add_to_base(self, base: &str) -> Cow<'_, str> { + let separator = if base.contains('?') { '&' } else { '?' }; + match self { + // This is the default, don't add anything + Self::True => Cow::Borrowed(base), + Self::False => format!("{base}{separator}count=false").into(), + Self::Only => format!("{base}{separator}count=only").into(), + } + } +} + #[derive(Deserialize, JsonSchema, Clone, Copy)] struct PaginationParams { /// Retrieve the items before the given ID @@ -83,6 +109,10 @@ struct PaginationParams { /// Retrieve the last N items #[serde(rename = "page[last]")] last: Option, + + /// Include the total number of items. Defaults to `true`. + #[serde(rename = "count")] + include_count: Option, } #[derive(Debug, thiserror::Error)] @@ -107,7 +137,7 @@ impl IntoResponse for PaginationRejection { /// An extractor for pagination parameters in the query string #[derive(OperationIo, Debug, Clone, Copy)] #[aide(input_with = "Query")] -pub struct Pagination(pub mas_storage::Pagination); +pub struct Pagination(pub mas_storage::Pagination, pub IncludeCount); impl FromRequestParts for Pagination { type Rejection = PaginationRejection; @@ -130,11 +160,14 @@ impl FromRequestParts for Pagination { (None, Some(last)) => (PaginationDirection::Backward, last.into()), }; - Ok(Self(mas_storage::Pagination { - before: params.before, - after: params.after, - direction, - count, - })) + Ok(Self( + mas_storage::Pagination { + before: params.before, + after: params.after, + direction, + count, + }, + params.include_count.unwrap_or_default(), + )) } } diff --git a/crates/handlers/src/admin/response.rs b/crates/handlers/src/admin/response.rs index 19f0e8040..257773cd2 100644 --- a/crates/handlers/src/admin/response.rs +++ b/crates/handlers/src/admin/response.rs @@ -6,7 +6,7 @@ #![allow(clippy::module_name_repetitions)] -use mas_storage::Pagination; +use mas_storage::{Pagination, pagination::Edge}; use schemars::JsonSchema; use serde::Serialize; use ulid::Ulid; @@ -21,10 +21,12 @@ struct PaginationLinks { self_: String, /// The link to the first page of results - first: String, + #[serde(skip_serializing_if = "Option::is_none")] + first: Option, /// The link to the last page of results - last: String, + #[serde(skip_serializing_if = "Option::is_none")] + last: Option, /// The link to the next page of results /// @@ -42,17 +44,27 @@ struct PaginationLinks { #[derive(Serialize, JsonSchema)] struct PaginationMeta { /// The total number of results - count: usize, + #[serde(skip_serializing_if = "Option::is_none")] + count: Option, +} + +impl PaginationMeta { + fn is_empty(&self) -> bool { + self.count.is_none() + } } /// A top-level response with a page of resources #[derive(Serialize, JsonSchema)] pub struct PaginatedResponse { /// Response metadata + #[serde(skip_serializing_if = "PaginationMeta::is_empty")] + #[schemars(with = "Option")] meta: PaginationMeta, /// The list of resources - data: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option>>, /// Related links links: PaginationLinks, @@ -87,22 +99,28 @@ fn url_with_pagination(base: &str, pagination: Pagination) -> String { } impl PaginatedResponse { - pub fn new( + pub fn for_page( page: mas_storage::Page, current_pagination: Pagination, - count: usize, + count: Option, base: &str, ) -> Self { let links = PaginationLinks { self_: url_with_pagination(base, current_pagination), - first: url_with_pagination(base, Pagination::first(current_pagination.count)), - last: url_with_pagination(base, Pagination::last(current_pagination.count)), + first: Some(url_with_pagination( + base, + Pagination::first(current_pagination.count), + )), + last: Some(url_with_pagination( + base, + Pagination::last(current_pagination.count), + )), next: page.has_next_page.then(|| { url_with_pagination( base, current_pagination .clear_before() - .after(page.edges.last().unwrap().id()), + .after(page.edges.last().unwrap().cursor), ) }), prev: if page.has_previous_page { @@ -110,18 +128,38 @@ impl PaginatedResponse { base, current_pagination .clear_after() - .before(page.edges.first().unwrap().id()), + .before(page.edges.first().unwrap().cursor), )) } else { None }, }; - let data = page.edges.into_iter().map(SingleResource::new).collect(); + let data = page + .edges + .into_iter() + .map(SingleResource::from_edge) + .collect(); Self { meta: PaginationMeta { count }, - data, + data: Some(data), + links, + } + } + + pub fn for_count_only(count: usize, base: &str) -> Self { + let links = PaginationLinks { + self_: base.to_owned(), + first: None, + last: None, + next: None, + prev: None, + }; + + Self { + meta: PaginationMeta { count: Some(count) }, + data: None, links, } } @@ -143,6 +181,32 @@ struct SingleResource { /// Related links links: SelfLinks, + + /// Metadata about the resource + #[serde(skip_serializing_if = "SingleResourceMeta::is_empty")] + #[schemars(with = "Option")] + meta: SingleResourceMeta, +} + +/// Metadata associated with a resource +#[derive(Serialize, JsonSchema)] +struct SingleResourceMeta { + /// Information about the pagination of the resource + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl SingleResourceMeta { + fn is_empty(&self) -> bool { + self.page.is_none() + } +} + +/// Pagination metadata for a resource +#[derive(Serialize, JsonSchema)] +struct SingleResourceMetaPage { + /// The cursor of this resource in the paginated result + cursor: String, } impl SingleResource { @@ -153,8 +217,16 @@ impl SingleResource { id: resource.id(), attributes: resource, links: SelfLinks { self_ }, + meta: SingleResourceMeta { page: None }, } } + + fn from_edge(edge: Edge) -> Self { + let cursor = edge.cursor.to_string(); + let mut resource = Self::new(edge.node); + resource.meta.page = Some(SingleResourceMetaPage { cursor }); + resource + } } /// Related links diff --git a/crates/handlers/src/admin/schema.rs b/crates/handlers/src/admin/schema.rs index 068c68977..e305df061 100644 --- a/crates/handlers/src/admin/schema.rs +++ b/crates/handlers/src/admin/schema.rs @@ -6,11 +6,9 @@ //! Common schema definitions -use schemars::{ - JsonSchema, - r#gen::SchemaGenerator, - schema::{InstanceType, Metadata, Schema, SchemaObject, StringValidation}, -}; +use std::borrow::Cow; + +use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema}; /// A type to use for schema definitions of ULIDs /// @@ -18,32 +16,21 @@ use schemars::{ pub struct Ulid; impl JsonSchema for Ulid { - fn schema_name() -> String { - "ULID".to_owned() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("ULID") } fn json_schema(_gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - - metadata: Some(Box::new(Metadata { - title: Some("ULID".into()), - description: Some("A ULID as per https://github.com/ulid/spec".into()), - examples: vec![ - "01ARZ3NDEKTSV4RRFFQ69G5FAV".into(), - "01J41912SC8VGAQDD50F6APK91".into(), - ], - ..Metadata::default() - })), - - string: Some(Box::new(StringValidation { - pattern: Some(r"^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$".into()), - ..StringValidation::default() - })), - - ..SchemaObject::default() - } - .into() + json_schema!({ + "type": "string", + "title": "ULID", + "description": "A ULID as per https://github.com/ulid/spec", + "examples": [ + "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "01J41912SC8VGAQDD50F6APK91", + ], + "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$", + }) } } @@ -53,27 +40,20 @@ impl JsonSchema for Ulid { pub struct Device; impl JsonSchema for Device { - fn schema_name() -> String { - "DeviceID".to_owned() + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("DeviceID") } fn json_schema(_gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - - metadata: Some(Box::new(Metadata { - title: Some("Device ID".into()), - examples: vec!["AABBCCDDEE".into(), "FFGGHHIIJJ".into()], - ..Metadata::default() - })), - - string: Some(Box::new(StringValidation { - pattern: Some(r"^[A-Za-z0-9._~!$&'()*+,;=:&/-]+$".into()), - ..StringValidation::default() - })), - - ..SchemaObject::default() - } - .into() + json_schema!({ + "type": "string", + "title": "Device ID", + "description": "A device ID as per https://matrix.org/docs/spec/client_server/r0.6.0#device-ids", + "examples": [ + "AABBCCDDEE", + "FFGGHHIIJJ", + ], + "pattern": "^[A-Za-z0-9._~!$&'()*+,;=:&/-]+$", + }) } } diff --git a/crates/handlers/src/admin/v1/compat_sessions/finish.rs b/crates/handlers/src/admin/v1/compat_sessions/finish.rs new file mode 100644 index 000000000..df42c2ff9 --- /dev/null +++ b/crates/handlers/src/admin/v1/compat_sessions/finish.rs @@ -0,0 +1,243 @@ +// 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. + +use aide::{NoApi, OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_data_model::BoxRng; +use mas_storage::queue::{QueueJobRepositoryExt as _, SyncDevicesJob}; +use ulid::Ulid; + +use crate::{ + admin::{ + call_context::CallContext, + model::{CompatSession, Resource}, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("Compatibility session with ID {0} not found")] + NotFound(Ulid), + + #[error("Compatibility session with ID {0} is already finished")] + AlreadyFinished(Ulid), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound(_) => StatusCode::NOT_FOUND, + Self::AlreadyFinished(_) => StatusCode::BAD_REQUEST, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("finishCompatSession") + .summary("Finish a compatibility session") + .description( + "Calling this endpoint will finish the compatibility session, preventing any further use. A job will be scheduled to sync the user's devices with the homeserver.", + ) + .tag("compat-session") + .response_with::<200, Json>, _>(|t| { + // Get the finished session sample + let [_, finished_session, _] = CompatSession::samples(); + let id = finished_session.id(); + let response = SingleResponse::new( + finished_session, + format!("/api/admin/v1/compat-sessions/{id}/finish"), + ); + t.description("Compatibility session was finished").example(response) + }) + .response_with::<400, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::AlreadyFinished(Ulid::nil())); + t.description("Session is already finished") + .example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil())); + t.description("Compatibility session was not found") + .example(response) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.compat_sessions.finish", skip_all)] +pub async fn handler( + CallContext { + mut repo, clock, .. + }: CallContext, + NoApi(mut rng): NoApi, + id: UlidPathParam, +) -> Result>, RouteError> { + let id = *id; + let session = repo + .compat_session() + .lookup(id) + .await? + .ok_or(RouteError::NotFound(id))?; + + // Check if the session is already finished + if session.finished_at().is_some() { + return Err(RouteError::AlreadyFinished(id)); + } + + // Schedule a job to sync the devices of the user with the homeserver + tracing::info!(user.id = %session.user_id, "Scheduling device sync job for user"); + repo.queue_job() + .schedule_job( + &mut rng, + &clock, + SyncDevicesJob::new_for_id(session.user_id), + ) + .await?; + + // Finish the session + let session = repo.compat_session().finish(&clock, session).await?; + + // Get the SSO login info for the response + let sso_login = repo.compat_sso_login().find_for_session(&session).await?; + + repo.save().await?; + + Ok(Json(SingleResponse::new( + CompatSession::from((session, sso_login)), + format!("/api/admin/v1/compat-sessions/{id}/finish"), + ))) +} + +#[cfg(test)] +mod tests { + use chrono::Duration; + use hyper::{Request, StatusCode}; + use mas_data_model::{Clock as _, Device}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + let mut rng = state.rng(); + + // Provision a user and a compat session + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let device = Device::generate(&mut rng); + let session = repo + .compat_session() + .add(&mut rng, &state.clock, &user, device, None, false, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let request = Request::post(format!( + "/api/admin/v1/compat-sessions/{}/finish", + session.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + // The finished_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["finished_at"], + serde_json::json!(state.clock.now()) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_already_finished_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + let mut rng = state.rng(); + + // Provision a user and a compat session + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let device = Device::generate(&mut rng); + let session = repo + .compat_session() + .add(&mut rng, &state.clock, &user, device, None, false, None) + .await + .unwrap(); + + // Finish the session first + let session = repo + .compat_session() + .finish(&state.clock, session) + .await + .unwrap(); + + repo.save().await.unwrap(); + + // Move the clock forward + state.clock.advance(Duration::try_minutes(1).unwrap()); + + let request = Request::post(format!( + "/api/admin/v1/compat-sessions/{}/finish", + session.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::BAD_REQUEST); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + format!( + "Compatibility session with ID {} is already finished", + session.id + ) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_unknown_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request = + Request::post("/api/admin/v1/compat-sessions/01040G2081040G2081040G2081/finish") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + "Compatibility session with ID 01040G2081040G2081040G2081 not found" + ); + } +} diff --git a/crates/handlers/src/admin/v1/compat_sessions/list.rs b/crates/handlers/src/admin/v1/compat_sessions/list.rs index debb2a304..b407854f6 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/list.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/list.rs @@ -4,11 +4,8 @@ // Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; -use axum::{ - Json, - extract::{Query, rejection::QueryRejection}, - response::IntoResponse, -}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_axum_utils::record_error; @@ -21,7 +18,7 @@ use crate::{ admin::{ call_context::CallContext, model::{CompatSession, Resource}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -137,16 +134,22 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p let sessions = CompatSession::samples(); let pagination = mas_storage::Pagination::first(sessions.len()); let page = Page { - edges: sessions.into(), + edges: sessions + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; t.description("Paginated response of compatibility sessions") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), CompatSession::PATH, )) }) @@ -159,10 +162,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p #[tracing::instrument(name = "handler.admin.v1.compat_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = CompatSession::PATH); + let base = include_count.add_to_base(&base); let filter = CompatSessionFilter::default(); // Load the user from the filter @@ -206,15 +210,31 @@ pub async fn handler( None => filter, }; - let page = repo.compat_session().list(filter, pagination).await?; - let count = repo.compat_session().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .compat_session() + .list(filter, pagination) + .await? + .map(CompatSession::from); + let count = repo.compat_session().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .compat_session() + .list(filter, pagination) + .await? + .map(CompatSession::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.compat_session().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(CompatSession::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] @@ -299,6 +319,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } } }, { @@ -318,6 +343,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW" + }, + "meta": { + "page": { + "cursor": "01FSHNCZP0PPF7X0EVMJNECPZW" + } } } ], @@ -362,6 +392,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } } } ], @@ -403,6 +438,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } } } ], @@ -444,6 +484,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW" + }, + "meta": { + "page": { + "cursor": "01FSHNCZP0PPF7X0EVMJNECPZW" + } } } ], @@ -454,5 +499,155 @@ mod tests { } } "#); + + // Test count=false + let request = Request::get("/api/admin/v1/compat-sessions?count=false") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "compat-session", + "id": "01FSHNB530AAPR7PEV8KNBZD5Y", + "attributes": { + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "device_id": "LoieH5Iecx", + "user_session_id": null, + "redirect_uri": null, + "created_at": "2022-01-16T14:41:00Z", + "user_agent": null, + "last_active_at": null, + "last_active_ip": null, + "finished_at": null, + "human_name": null + }, + "links": { + "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } + } + }, + { + "type": "compat-session", + "id": "01FSHNCZP0PPF7X0EVMJNECPZW", + "attributes": { + "user_id": "01FSHNB530AJ6AC5HQ9X6H4RP4", + "device_id": "ZXyvelQWW9", + "user_session_id": null, + "redirect_uri": null, + "created_at": "2022-01-16T14:42:00Z", + "user_agent": null, + "last_active_at": null, + "last_active_ip": null, + "finished_at": "2022-01-16T14:43:00Z", + "human_name": null + }, + "links": { + "self": "/api/admin/v1/compat-sessions/01FSHNCZP0PPF7X0EVMJNECPZW" + }, + "meta": { + "page": { + "cursor": "01FSHNCZP0PPF7X0EVMJNECPZW" + } + } + } + ], + "links": { + "self": "/api/admin/v1/compat-sessions?count=false&page[first]=10", + "first": "/api/admin/v1/compat-sessions?count=false&page[first]=10", + "last": "/api/admin/v1/compat-sessions?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/compat-sessions?count=only") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 2 + }, + "links": { + "self": "/api/admin/v1/compat-sessions?count=only" + } + } + "#); + + // Test count=false with filtering + let request = Request::get(format!( + "/api/admin/v1/compat-sessions?count=false&filter[user]={}", + alice.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "compat-session", + "id": "01FSHNB530AAPR7PEV8KNBZD5Y", + "attributes": { + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "device_id": "LoieH5Iecx", + "user_session_id": null, + "redirect_uri": null, + "created_at": "2022-01-16T14:41:00Z", + "user_agent": null, + "last_active_at": null, + "last_active_ip": null, + "finished_at": null, + "human_name": null + }, + "links": { + "self": "/api/admin/v1/compat-sessions/01FSHNB530AAPR7PEV8KNBZD5Y" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AAPR7PEV8KNBZD5Y" + } + } + } + ], + "links": { + "self": "/api/admin/v1/compat-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "first": "/api/admin/v1/compat-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "last": "/api/admin/v1/compat-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[last]=10" + } + } + "#); + + // Test count=only with filtering + let request = + Request::get("/api/admin/v1/compat-sessions?count=only&filter[status]=active") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "links": { + "self": "/api/admin/v1/compat-sessions?filter[status]=active&count=only" + } + } + "#); } } diff --git a/crates/handlers/src/admin/v1/compat_sessions/mod.rs b/crates/handlers/src/admin/v1/compat_sessions/mod.rs index 18ffe5af6..db7b17ff5 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/mod.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/mod.rs @@ -3,10 +3,12 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +mod finish; mod get; mod list; pub use self::{ + finish::{doc as finish_doc, handler as finish}, get::{doc as get_doc, handler as get}, list::{doc as list_doc, handler as list}, }; diff --git a/crates/handlers/src/admin/v1/mod.rs b/crates/handlers/src/admin/v1/mod.rs index 49ff75001..98f1d10e2 100644 --- a/crates/handlers/src/admin/v1/mod.rs +++ b/crates/handlers/src/admin/v1/mod.rs @@ -11,7 +11,7 @@ use aide::axum::{ routing::{get_with, post_with}, }; use axum::extract::{FromRef, FromRequestParts}; -use mas_data_model::BoxRng; +use mas_data_model::{AppVersion, BoxRng, SiteConfig}; use mas_matrix::HomeserverConnection; use mas_policy::PolicyFactory; @@ -20,23 +20,37 @@ use crate::passwords::PasswordManager; mod compat_sessions; mod oauth2_sessions; +mod personal_sessions; mod policy_data; +mod site_config; mod upstream_oauth_links; +mod upstream_oauth_providers; mod user_emails; mod user_registration_tokens; mod user_sessions; mod users; +mod version; pub fn router() -> ApiRouter where S: Clone + Send + Sync + 'static, Arc: FromRef, PasswordManager: FromRef, + SiteConfig: FromRef, + AppVersion: FromRef, Arc: FromRef, BoxRng: FromRequestParts, CallContext: FromRequestParts, { ApiRouter::::new() + .api_route( + "/site-config", + get_with(self::site_config::handler, self::site_config::doc), + ) + .api_route( + "/version", + get_with(self::version::handler, self::version::doc), + ) .api_route( "/compat-sessions", get_with(self::compat_sessions::list, self::compat_sessions::list_doc), @@ -45,6 +59,13 @@ where "/compat-sessions/{id}", get_with(self::compat_sessions::get, self::compat_sessions::get_doc), ) + .api_route( + "/compat-sessions/{id}/finish", + post_with( + self::compat_sessions::finish, + self::compat_sessions::finish_doc, + ), + ) .api_route( "/oauth2-sessions", get_with(self::oauth2_sessions::list, self::oauth2_sessions::list_doc), @@ -53,6 +74,45 @@ where "/oauth2-sessions/{id}", get_with(self::oauth2_sessions::get, self::oauth2_sessions::get_doc), ) + .api_route( + "/oauth2-sessions/{id}/finish", + post_with( + self::oauth2_sessions::finish, + self::oauth2_sessions::finish_doc, + ), + ) + .api_route( + "/personal-sessions", + get_with( + self::personal_sessions::list, + self::personal_sessions::list_doc, + ) + .post_with( + self::personal_sessions::add, + self::personal_sessions::add_doc, + ), + ) + .api_route( + "/personal-sessions/{id}", + get_with( + self::personal_sessions::get, + self::personal_sessions::get_doc, + ), + ) + .api_route( + "/personal-sessions/{id}/revoke", + post_with( + self::personal_sessions::revoke, + self::personal_sessions::revoke_doc, + ), + ) + .api_route( + "/personal-sessions/{id}/regenerate", + post_with( + self::personal_sessions::regenerate, + self::personal_sessions::regenerate_doc, + ), + ) .api_route( "/policy-data", post_with(self::policy_data::set, self::policy_data::set_doc), @@ -123,6 +183,10 @@ where "/user-sessions/{id}", get_with(self::user_sessions::get, self::user_sessions::get_doc), ) + .api_route( + "/user-sessions/{id}/finish", + post_with(self::user_sessions::finish, self::user_sessions::finish_doc), + ) .api_route( "/user-registration-tokens", get_with( @@ -181,4 +245,18 @@ where self::upstream_oauth_links::delete_doc, ), ) + .api_route( + "/upstream-oauth-providers", + get_with( + self::upstream_oauth_providers::list, + self::upstream_oauth_providers::list_doc, + ), + ) + .api_route( + "/upstream-oauth-providers/{id}", + get_with( + self::upstream_oauth_providers::get, + self::upstream_oauth_providers::get_doc, + ), + ) } diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/finish.rs b/crates/handlers/src/admin/v1/oauth2_sessions/finish.rs new file mode 100644 index 000000000..23edef30a --- /dev/null +++ b/crates/handlers/src/admin/v1/oauth2_sessions/finish.rs @@ -0,0 +1,234 @@ +// 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. + +use aide::{NoApi, OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_data_model::BoxRng; +use mas_storage::queue::{QueueJobRepositoryExt as _, SyncDevicesJob}; +use ulid::Ulid; + +use crate::{ + admin::{ + call_context::CallContext, + model::{OAuth2Session, Resource}, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("OAuth 2.0 session with ID {0} not found")] + NotFound(Ulid), + + #[error("OAuth 2.0 session with ID {0} is already finished")] + AlreadyFinished(Ulid), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound(_) => StatusCode::NOT_FOUND, + Self::AlreadyFinished(_) => StatusCode::BAD_REQUEST, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("finishOAuth2Session") + .summary("Finish an OAuth 2.0 session") + .description( + "Calling this endpoint will finish the OAuth 2.0 session, preventing any further use. If the session has a user associated with it, a job will be scheduled to sync the user's devices with the homeserver.", + ) + .tag("oauth2-session") + .response_with::<200, Json>, _>(|t| { + // Get the finished session sample + let [_, _, finished_session] = OAuth2Session::samples(); + let id = finished_session.id(); + let response = SingleResponse::new( + finished_session, + format!("/api/admin/v1/oauth2-sessions/{id}/finish"), + ); + t.description("OAuth 2.0 session was finished").example(response) + }) + .response_with::<400, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::AlreadyFinished(Ulid::nil())); + t.description("Session is already finished") + .example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil())); + t.description("OAuth 2.0 session was not found") + .example(response) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.oauth2_sessions.finish", skip_all)] +pub async fn handler( + CallContext { + mut repo, clock, .. + }: CallContext, + NoApi(mut rng): NoApi, + id: UlidPathParam, +) -> Result>, RouteError> { + let id = *id; + let session = repo + .oauth2_session() + .lookup(id) + .await? + .ok_or(RouteError::NotFound(id))?; + + // Check if the session is already finished + if session.finished_at().is_some() { + return Err(RouteError::AlreadyFinished(id)); + } + + // If the session has a user associated with it, schedule a job to sync devices + if let Some(user_id) = session.user_id { + tracing::info!(user.id = %user_id, "Scheduling device sync job for user"); + let job = SyncDevicesJob::new_for_id(user_id); + repo.queue_job().schedule_job(&mut rng, &clock, job).await?; + } + + // Finish the session + let session = repo.oauth2_session().finish(&clock, session).await?; + + repo.save().await?; + + Ok(Json(SingleResponse::new( + OAuth2Session::from(session), + format!("/api/admin/v1/oauth2-sessions/{id}/finish"), + ))) +} + +#[cfg(test)] +mod tests { + use chrono::Duration; + use hyper::{Request, StatusCode}; + use mas_data_model::{AccessToken, Clock as _}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + // Get the session ID from the token we just created + let mut repo = state.repository().await.unwrap(); + let AccessToken { session_id, .. } = repo + .oauth2_access_token() + .find_by_token(&token) + .await + .unwrap() + .unwrap(); + repo.save().await.unwrap(); + + let request = Request::post(format!("/api/admin/v1/oauth2-sessions/{session_id}/finish")) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + // The finished_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["finished_at"], + serde_json::json!(state.clock.now()) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_already_finished_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + + // Create first admin token for the API call + let admin_token = state.token_with_scope("urn:mas:admin").await; + + // Create a second admin session that we'll finish + let second_admin_token = state.token_with_scope("urn:mas:admin").await; + + // Get the second session and finish it first + let mut repo = state.repository().await.unwrap(); + let AccessToken { session_id, .. } = repo + .oauth2_access_token() + .find_by_token(&second_admin_token) + .await + .unwrap() + .unwrap(); + + let session = repo + .oauth2_session() + .lookup(session_id) + .await + .unwrap() + .unwrap(); + + // Finish the session first + let session = repo + .oauth2_session() + .finish(&state.clock, session) + .await + .unwrap(); + + repo.save().await.unwrap(); + + // Move the clock forward + state.clock.advance(Duration::try_minutes(1).unwrap()); + + let request = Request::post(format!( + "/api/admin/v1/oauth2-sessions/{}/finish", + session.id + )) + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::BAD_REQUEST); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + format!( + "OAuth 2.0 session with ID {} is already finished", + session.id + ) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_unknown_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request = + Request::post("/api/admin/v1/oauth2-sessions/01040G2081040G2081040G2081/finish") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + "OAuth 2.0 session with ID 01040G2081040G2081040G2081 not found" + ); + } +} diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs index 52b597edc..37f6ed378 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs @@ -7,11 +7,8 @@ use std::str::FromStr; use aide::{OperationIo, transform::TransformOperation}; -use axum::{ - Json, - extract::{Query, rejection::QueryRejection}, - response::IntoResponse, -}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_axum_utils::record_error; @@ -25,7 +22,7 @@ use crate::{ admin::{ call_context::CallContext, model::{OAuth2Session, Resource}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -192,16 +189,22 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p let sessions = OAuth2Session::samples(); let pagination = mas_storage::Pagination::first(sessions.len()); let page = Page { - edges: sessions.into(), + edges: sessions + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; t.description("Paginated response of OAuth 2.0 sessions") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), OAuth2Session::PATH, )) }) @@ -218,10 +221,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p #[tracing::instrument(name = "handler.admin.v1.oauth2_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = OAuth2Session::PATH); + let base = include_count.add_to_base(&base); let filter = OAuth2SessionFilter::default(); // Load the user from the filter @@ -300,15 +304,31 @@ pub async fn handler( None => filter, }; - let page = repo.oauth2_session().list(filter, pagination).await?; - let count = repo.oauth2_session().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .oauth2_session() + .list(filter, pagination) + .await? + .map(OAuth2Session::from); + let count = repo.oauth2_session().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .oauth2_session() + .list(filter, pagination) + .await? + .map(OAuth2Session::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.oauth2_session().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(OAuth2Session::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] @@ -354,6 +374,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MKGTBNZ16RDR3PVY" + } } } ], @@ -364,5 +389,66 @@ mod tests { } } "#); + + // Test count=false + let request = Request::get("/api/admin/v1/oauth2-sessions?count=false") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "oauth2-session", + "id": "01FSHN9AG0MKGTBNZ16RDR3PVY", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "finished_at": null, + "user_id": null, + "user_session_id": null, + "client_id": "01FSHN9AG0FAQ50MT1E9FFRPZR", + "scope": "urn:mas:admin", + "user_agent": null, + "last_active_at": null, + "last_active_ip": null, + "human_name": null + }, + "links": { + "self": "/api/admin/v1/oauth2-sessions/01FSHN9AG0MKGTBNZ16RDR3PVY" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MKGTBNZ16RDR3PVY" + } + } + } + ], + "links": { + "self": "/api/admin/v1/oauth2-sessions?count=false&page[first]=10", + "first": "/api/admin/v1/oauth2-sessions?count=false&page[first]=10", + "last": "/api/admin/v1/oauth2-sessions?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/oauth2-sessions?count=only") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "links": { + "self": "/api/admin/v1/oauth2-sessions?count=only" + } + } + "#); } } diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs b/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs index 9b6272cef..5ac2e049e 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs @@ -4,10 +4,12 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +mod finish; mod get; mod list; pub use self::{ + finish::{doc as finish_doc, handler as finish}, get::{doc as get_doc, handler as get}, list::{doc as list_doc, handler as list}, }; diff --git a/crates/handlers/src/admin/v1/personal_sessions/add.rs b/crates/handlers/src/admin/v1/personal_sessions/add.rs new file mode 100644 index 000000000..2cfe1fb88 --- /dev/null +++ b/crates/handlers/src/admin/v1/personal_sessions/add.rs @@ -0,0 +1,311 @@ +// 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. + +use std::sync::Arc; + +use aide::{NoApi, OperationIo, transform::TransformOperation}; +use anyhow::Context; +use axum::{Json, extract::State, response::IntoResponse}; +use chrono::Duration; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_data_model::{BoxRng, Device, TokenType}; +use mas_matrix::HomeserverConnection; +use oauth2_types::scope::Scope; +use schemars::JsonSchema; +use serde::Deserialize; +use ulid::Ulid; + +use crate::{ + admin::{ + call_context::CallContext, + model::{InconsistentPersonalSession, PersonalSession}, + response::{ErrorResponse, SingleResponse}, + v1::personal_sessions::personal_session_owner_from_caller, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("User not found")] + UserNotFound, + + #[error("User is not active")] + UserDeactivated, + + #[error("Invalid scope")] + InvalidScope, +} + +impl_from_error_for_route!(mas_storage::RepositoryError); +impl_from_error_for_route!(InconsistentPersonalSession); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::UserNotFound => StatusCode::NOT_FOUND, + Self::UserDeactivated => StatusCode::GONE, + Self::InvalidScope => StatusCode::BAD_REQUEST, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +/// # JSON payload for the `POST /api/admin/v1/personal-sessions` endpoint +#[derive(Deserialize, JsonSchema)] +#[serde(rename = "CreatePersonalSessionRequest")] +pub struct Request { + /// The user this session will act on behalf of + #[schemars(with = "crate::admin::schema::Ulid")] + actor_user_id: Ulid, + + /// Human-readable name for the session + human_name: String, + + /// `OAuth2` scopes for this session + scope: String, + + /// Token expiry time in seconds. + /// If not set, the token won't expire. + expires_in: Option, +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("createPersonalSession") + .summary("Create a new personal session with personal access token") + .tag("personal-session") + .response_with::<201, Json>, _>(|t| { + t.description("Personal session and personal access token were created") + }) + .response_with::<400, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::InvalidScope); + t.description("Invalid scope provided").example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::UserNotFound); + t.description("User was not found").example(response) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.personal_sessions.add", skip_all)] +pub async fn handler( + CallContext { + mut repo, + clock, + session, + .. + }: CallContext, + NoApi(mut rng): NoApi, + NoApi(State(homeserver)): NoApi>>, + Json(params): Json, +) -> Result<(StatusCode, Json>), RouteError> { + let owner = personal_session_owner_from_caller(&session); + + let actor_user = repo + .user() + .lookup(params.actor_user_id) + .await? + .ok_or(RouteError::UserNotFound)?; + + if !actor_user.is_valid_actor() { + return Err(RouteError::UserDeactivated); + } + + let scope: Scope = params.scope.parse().map_err(|_| RouteError::InvalidScope)?; + + // Create the personal session + let session = repo + .personal_session() + .add( + &mut rng, + &clock, + owner, + &actor_user, + params.human_name, + scope, + ) + .await?; + + // Create the initial token for the session + let access_token_string = TokenType::PersonalAccessToken.generate(&mut rng); + let access_token = repo + .personal_access_token() + .add( + &mut rng, + &clock, + &session, + &access_token_string, + params + .expires_in + .map(|exp_in| Duration::seconds(i64::from(exp_in))), + ) + .await?; + + // If the session has a device, we should add those to the homeserver now + if session.has_device() { + // Lock the user sync to make sure we don't get into a race condition + repo.user().acquire_lock_for_sync(&actor_user).await?; + + for scope in &*session.scope { + if let Some(device) = Device::from_scope_token(scope) { + // NOTE: We haven't relinquished the repo at this point, + // so we are holding a transaction across the homeserver + // operation. + // This is suboptimal, but simpler. + // Given this is an administrative endpoint, this is a tolerable + // compromise for now. + homeserver + .upsert_device(&actor_user.username, device.as_str(), None) + .await + .context("Failed to provision device") + .map_err(|e| RouteError::Internal(e.into()))?; + } + } + } + + repo.save().await?; + + Ok(( + StatusCode::CREATED, + Json(SingleResponse::new_canonical( + PersonalSession::try_from((session, Some(access_token)))? + .with_token(access_token_string), + )), + )) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use insta::assert_json_snapshot; + use serde_json::Value; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_create_personal_session_with_token(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + // Create a user for testing + let mut repo = state.repository().await.unwrap(); + let mut rng = state.rng(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let request_body = serde_json::json!({ + "actor_user_id": user.id, + "human_name": "Test Session", + "scope": "openid urn:mas:admin", + "expires_in": 3600 + }); + + let request = Request::post("/api/admin/v1/personal-sessions") + .bearer(&token) + .json(&request_body); + + let response = state.request(request).await; + response.assert_status(StatusCode::CREATED); + + let body: Value = response.json(); + + assert_json_snapshot!(body, @r#" + { + "data": { + "type": "personal-session", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "revoked_at": null, + "owner_user_id": null, + "owner_client_id": "01FSHN9AG0FAQ50MT1E9FFRPZR", + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_name": "Test Session", + "scope": "openid urn:mas:admin", + "last_active_at": null, + "last_active_ip": null, + "expires_at": "2022-01-16T15:40:00Z", + "access_token": "mpt_FM44zJN5qePGMLvvMXC4Ds1A3lCWc6_bJ9Wj1" + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG07HNEZXNQM2KNBNF6" + } + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_create_personal_session_invalid_user(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request_body = serde_json::json!({ + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "scope": "openid", + "human_name": "Test Session", + "expires_in": 3600 + }); + + let request = Request::post("/api/admin/v1/personal-sessions") + .bearer(&token) + .json(&request_body); + + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_create_personal_session_invalid_scope(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + // Create a user for testing + let mut repo = state.repository().await.unwrap(); + let mut rng = state.rng(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let request_body = serde_json::json!({ + "actor_user_id": user.id, + "human_name": "Test Session", + "scope": "invalid\nscope", + "expires_in": 3600 + }); + + let request = Request::post("/api/admin/v1/personal-sessions") + .bearer(&token) + .json(&request_body); + + let response = state.request(request).await; + response.assert_status(StatusCode::BAD_REQUEST); + } +} diff --git a/crates/handlers/src/admin/v1/personal_sessions/get.rs b/crates/handlers/src/admin/v1/personal_sessions/get.rs new file mode 100644 index 000000000..c0c0378f8 --- /dev/null +++ b/crates/handlers/src/admin/v1/personal_sessions/get.rs @@ -0,0 +1,189 @@ +// 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. + +use aide::{OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use hyper::StatusCode; +use mas_axum_utils::record_error; + +use crate::{ + admin::{ + call_context::CallContext, + model::{InconsistentPersonalSession, PersonalSession}, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("Personal session not found")] + NotFound, +} + +impl_from_error_for_route!(mas_storage::RepositoryError); +impl_from_error_for_route!(InconsistentPersonalSession); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound => StatusCode::NOT_FOUND, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("getPersonalSession") + .summary("Get a personal session") + .tag("personal-session") + .response_with::<200, Json>, _>(|t| { + let [sample, ..] = PersonalSession::samples(); + let response = SingleResponse::new_canonical(sample); + t.description("Personal session details").example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::NotFound); + t.description("Personal session not found") + .example(response) + }) +} + +#[tracing::instrument( + name = "handler.admin.v1.personal_sessions.get", + skip_all, + fields(personal_session.id = %*id), +)] +pub async fn handler( + CallContext { mut repo, .. }: CallContext, + id: UlidPathParam, +) -> Result>, RouteError> { + let session_id = *id; + + let session = repo + .personal_session() + .lookup(session_id) + .await? + .ok_or(RouteError::NotFound)?; + + let token = if session.is_revoked() { + None + } else { + repo.personal_access_token() + .find_active_for_session(&session) + .await? + }; + + Ok(Json(SingleResponse::new_canonical( + PersonalSession::try_from((session, token))?, + ))) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use insta::assert_json_snapshot; + use mas_data_model::personal::session::PersonalSessionOwner; + use oauth2_types::scope::{OPENID, Scope}; + use sqlx::PgPool; + use ulid::Ulid; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_get(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + // Create a user and personal session for testing + let mut repo = state.repository().await.unwrap(); + let mut rng = state.rng(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + + let personal_session = repo + .personal_session() + .add( + &mut rng, + &state.clock, + PersonalSessionOwner::from(&user), + &user, + "Test session".to_owned(), + Scope::from_iter([OPENID]), + ) + .await + .unwrap(); + repo.personal_access_token() + .add(&mut rng, &state.clock, &personal_session, "mpt_hiss", None) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let request = Request::get(format!( + "/api/admin/v1/personal-sessions/{}", + personal_session.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_eq!(body["data"]["id"], personal_session.id.to_string()); + assert_json_snapshot!(body, @r#" + { + "data": { + "type": "personal-session", + "id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "revoked_at": null, + "owner_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_name": "Test session", + "scope": "openid", + "last_active_at": null, + "last_active_ip": null, + "expires_at": null + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_not_found(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let session_id = Ulid::nil(); + let request = Request::get(format!("/api/admin/v1/personal-sessions/{session_id}")) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + } +} diff --git a/crates/handlers/src/admin/v1/personal_sessions/list.rs b/crates/handlers/src/admin/v1/personal_sessions/list.rs new file mode 100644 index 000000000..c9d3d55d4 --- /dev/null +++ b/crates/handlers/src/admin/v1/personal_sessions/list.rs @@ -0,0 +1,585 @@ +// 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. + +use std::str::FromStr as _; + +use aide::{OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; +use axum_macros::FromRequestParts; +use chrono::{DateTime, Utc}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_storage::personal::PersonalSessionFilter; +use oauth2_types::scope::{Scope, ScopeToken}; +use schemars::JsonSchema; +use serde::Deserialize; +use ulid::Ulid; + +use crate::{ + admin::{ + call_context::CallContext, + model::{InconsistentPersonalSession, PersonalSession, Resource}, + params::{IncludeCount, Pagination}, + response::{ErrorResponse, PaginatedResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Deserialize, JsonSchema, Clone, Copy)] +#[serde(rename_all = "snake_case")] +enum PersonalSessionStatus { + Active, + Revoked, +} + +impl std::fmt::Display for PersonalSessionStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Active => write!(f, "active"), + Self::Revoked => write!(f, "revoked"), + } + } +} + +#[derive(FromRequestParts, Deserialize, JsonSchema, OperationIo)] +#[serde(rename = "PersonalSessionFilter")] +#[aide(input_with = "Query")] +#[from_request(via(Query), rejection(RouteError))] +pub struct FilterParams { + /// Filter by owner user ID + #[serde(rename = "filter[owner_user]")] + #[schemars(with = "Option")] + owner_user: Option, + + /// Filter by owner `OAuth2` client ID + #[serde(rename = "filter[owner_client]")] + #[schemars(with = "Option")] + owner_client: Option, + + /// Filter by actor user ID + #[serde(rename = "filter[actor_user]")] + #[schemars(with = "Option")] + actor_user: Option, + + /// Retrieve the items with the given scope + #[serde(default, rename = "filter[scope]")] + scope: Vec, + + /// Filter by session status + #[serde(rename = "filter[status]")] + status: Option, + + /// Filter by access token expiry date + #[serde(rename = "filter[expires_before]")] + expires_before: Option>, + + /// Filter by access token expiry date + #[serde(rename = "filter[expires_after]")] + expires_after: Option>, + + /// Filter by whether the access token has an expiry time + #[serde(rename = "filter[expires]")] + expires: Option, +} + +impl std::fmt::Display for FilterParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut sep = '?'; + + if let Some(owner_user) = self.owner_user { + write!(f, "{sep}filter[owner_user]={owner_user}")?; + sep = '&'; + } + if let Some(owner_client) = self.owner_client { + write!(f, "{sep}filter[owner_client]={owner_client}")?; + sep = '&'; + } + if let Some(actor_user) = self.actor_user { + write!(f, "{sep}filter[actor_user]={actor_user}")?; + sep = '&'; + } + for scope in &self.scope { + write!(f, "{sep}filter[scope]={scope}")?; + sep = '&'; + } + if let Some(status) = self.status { + write!(f, "{sep}filter[status]={status}")?; + sep = '&'; + } + if let Some(expires_before) = self.expires_before { + write!( + f, + "{sep}filter[expires_before]={}", + expires_before.format("%Y-%m-%dT%H:%M:%SZ") + )?; + sep = '&'; + } + if let Some(expires_after) = self.expires_after { + write!( + f, + "{sep}filter[expires_after]={}", + expires_after.format("%Y-%m-%dT%H:%M:%SZ") + )?; + sep = '&'; + } + if let Some(expires) = self.expires { + write!(f, "{sep}filter[expires]={expires}")?; + sep = '&'; + } + + let _ = sep; + Ok(()) + } +} + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("User ID {0} not found")] + UserNotFound(Ulid), + + #[error("Client ID {0} not found")] + ClientNotFound(Ulid), + + #[error("Invalid filter parameters")] + InvalidFilter(#[from] QueryRejection), + + #[error("Invalid scope {0:?} in filter parameters")] + InvalidScope(String), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); +impl_from_error_for_route!(InconsistentPersonalSession); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::UserNotFound(_) | Self::ClientNotFound(_) => StatusCode::NOT_FOUND, + Self::InvalidScope(_) | Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("listPersonalSessions") + .summary("List personal sessions") + .description("Retrieve a list of personal sessions. +Note that by default, all sessions, including revoked ones are returned, with the oldest first. +Use the `filter[status]` parameter to filter the sessions by their status and `page[last]` parameter to retrieve the last N sessions.") + .tag("personal-session") + .response_with::<200, Json>, _>(|t| { + let sessions = PersonalSession::samples(); + let pagination = mas_storage::Pagination::first(sessions.len()); + let page = mas_storage::Page { + edges: sessions + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), + has_next_page: true, + has_previous_page: false, + }; + + t.description("Paginated response of personal sessions") + .example(PaginatedResponse::for_page( + page, + pagination, + Some(3), + PersonalSession::PATH, + )) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::UserNotFound(Ulid::nil())); + t.description("User was not found").example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::ClientNotFound(Ulid::nil())); + t.description("Client was not found").example(response) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.personal_sessions.list", skip_all)] +pub async fn handler( + CallContext { mut repo, .. }: CallContext, + Pagination(pagination, include_count): Pagination, + params: FilterParams, +) -> Result>, RouteError> { + let base = format!("{path}{params}", path = PersonalSession::PATH); + let base = include_count.add_to_base(&base); + + let filter = PersonalSessionFilter::new(); + + let owner_user = if let Some(owner_user_id) = params.owner_user { + let owner_user = repo + .user() + .lookup(owner_user_id) + .await? + .ok_or(RouteError::UserNotFound(owner_user_id))?; + Some(owner_user) + } else { + None + }; + + let filter = match &owner_user { + Some(user) => filter.for_owner_user(user), + None => filter, + }; + + let owner_client = if let Some(owner_client_id) = params.owner_client { + let owner_client = repo + .oauth2_client() + .lookup(owner_client_id) + .await? + .ok_or(RouteError::ClientNotFound(owner_client_id))?; + Some(owner_client) + } else { + None + }; + + let filter = match &owner_client { + Some(client) => filter.for_owner_oauth2_client(client), + None => filter, + }; + + let actor_user = if let Some(actor_user_id) = params.actor_user { + let user = repo + .user() + .lookup(actor_user_id) + .await? + .ok_or(RouteError::UserNotFound(actor_user_id))?; + Some(user) + } else { + None + }; + + let filter = match &actor_user { + Some(user) => filter.for_actor_user(user), + None => filter, + }; + + let scope: Scope = params + .scope + .into_iter() + .map(|s| ScopeToken::from_str(&s).map_err(|_| RouteError::InvalidScope(s))) + .collect::>()?; + + let filter = if scope.is_empty() { + filter + } else { + filter.with_scope(&scope) + }; + + let filter = match params.status { + Some(PersonalSessionStatus::Active) => filter.active_only(), + Some(PersonalSessionStatus::Revoked) => filter.finished_only(), + None => filter, + }; + + let filter = if let Some(expires_after) = params.expires_after { + filter.with_expires_after(expires_after) + } else { + filter + }; + + let filter = if let Some(expires_before) = params.expires_before { + filter.with_expires_before(expires_before) + } else { + filter + }; + + let filter = if let Some(expires) = params.expires { + filter.with_expires(expires) + } else { + filter + }; + + let response = match include_count { + IncludeCount::True => { + let page = repo.personal_session().list(filter, pagination).await?; + let count = repo.personal_session().count(filter).await?; + PaginatedResponse::for_page( + page.try_map(PersonalSession::try_from)?, + pagination, + Some(count), + &base, + ) + } + IncludeCount::False => { + let page = repo.personal_session().list(filter, pagination).await?; + PaginatedResponse::for_page( + page.try_map(PersonalSession::try_from)?, + pagination, + None, + &base, + ) + } + IncludeCount::Only => { + let count = repo.personal_session().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; + + Ok(Json(response)) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use chrono::Duration; + use hyper::{Request, StatusCode}; + use insta::assert_json_snapshot; + use mas_data_model::personal::session::PersonalSessionOwner; + use oauth2_types::scope::{OPENID, Scope}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_list(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + + // Create a user and personal session for testing + let mut repo = state.repository().await.unwrap(); + let mut rng = state.rng(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + + let personal_session = repo + .personal_session() + .add( + &mut rng, + &state.clock, + PersonalSessionOwner::from(&user), + &user, + "Test session".to_owned(), + Scope::from_iter([OPENID]), + ) + .await + .unwrap(); + repo.personal_access_token() + .add( + &mut rng, + &state.clock, + &personal_session, + "mpt_hiss", + Some(Duration::days(42)), + ) + .await + .unwrap(); + + state.clock.advance(Duration::days(1)); + + let personal_session = repo + .personal_session() + .add( + &mut rng, + &state.clock, + PersonalSessionOwner::from(&user), + &user, + "Another test session".to_owned(), + Scope::from_iter([OPENID]), + ) + .await + .unwrap(); + repo.personal_access_token() + .add( + &mut rng, + &state.clock, + &personal_session, + "mpt_scratch", + Some(Duration::days(21)), + ) + .await + .unwrap(); + repo.personal_session() + .revoke(&state.clock, personal_session) + .await + .unwrap(); + + state.clock.advance(Duration::days(1)); + + let personal_session = repo + .personal_session() + .add( + &mut rng, + &state.clock, + PersonalSessionOwner::from(&user), + &user, + "Another test session".to_owned(), + Scope::from_iter([OPENID, "urn:mas:admin".parse().unwrap()]), + ) + .await + .unwrap(); + repo.personal_access_token() + .add( + &mut rng, + &state.clock, + &personal_session, + "mpt_meow", + Some(Duration::days(14)), + ) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let token = state.token_with_scope("urn:mas:admin").await; + let request = Request::get("/api/admin/v1/personal-sessions") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "personal-session", + "id": "01FSHN9AG0YQYAR04VCYTHJ8SK", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "revoked_at": null, + "owner_user_id": "01FSHN9AG09FE39KETP6F390F8", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG09FE39KETP6F390F8", + "human_name": "Test session", + "scope": "openid", + "last_active_at": null, + "last_active_ip": null, + "expires_at": "2022-02-27T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0YQYAR04VCYTHJ8SK" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0YQYAR04VCYTHJ8SK" + } + } + }, + { + "type": "personal-session", + "id": "01FSM7P1G0VBGAMK9D9QMGQ5MY", + "attributes": { + "created_at": "2022-01-17T14:40:00Z", + "revoked_at": "2022-01-17T14:40:00Z", + "owner_user_id": "01FSHN9AG09FE39KETP6F390F8", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG09FE39KETP6F390F8", + "human_name": "Another test session", + "scope": "openid", + "last_active_at": null, + "last_active_ip": null, + "expires_at": null + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSM7P1G0VBGAMK9D9QMGQ5MY" + }, + "meta": { + "page": { + "cursor": "01FSM7P1G0VBGAMK9D9QMGQ5MY" + } + } + }, + { + "type": "personal-session", + "id": "01FSPT2RG08Y11Y5BM4VZ4CN8K", + "attributes": { + "created_at": "2022-01-18T14:40:00Z", + "revoked_at": null, + "owner_user_id": "01FSHN9AG09FE39KETP6F390F8", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG09FE39KETP6F390F8", + "human_name": "Another test session", + "scope": "openid urn:mas:admin", + "last_active_at": null, + "last_active_ip": null, + "expires_at": "2022-02-01T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSPT2RG08Y11Y5BM4VZ4CN8K" + }, + "meta": { + "page": { + "cursor": "01FSPT2RG08Y11Y5BM4VZ4CN8K" + } + } + } + ], + "links": { + "self": "/api/admin/v1/personal-sessions?page[first]=10", + "first": "/api/admin/v1/personal-sessions?page[first]=10", + "last": "/api/admin/v1/personal-sessions?page[last]=10" + } + } + "#); + + // Map of filters to their expected set of returned ULIDs + let filters_and_expected: &[(&str, &[&str])] = &[ + ( + "filter[expires_before]=2022-02-15T00:00:00Z", + &["01FSPT2RG08Y11Y5BM4VZ4CN8K"], + ), + ( + "filter[expires_after]=2022-02-15T00:00:00Z", + &["01FSHN9AG0YQYAR04VCYTHJ8SK"], + ), + ( + "filter[status]=active", + &["01FSHN9AG0YQYAR04VCYTHJ8SK", "01FSPT2RG08Y11Y5BM4VZ4CN8K"], + ), + ("filter[status]=revoked", &["01FSM7P1G0VBGAMK9D9QMGQ5MY"]), + ( + "filter[expires]=true", + &["01FSHN9AG0YQYAR04VCYTHJ8SK", "01FSPT2RG08Y11Y5BM4VZ4CN8K"], + ), + ("filter[expires]=false", &["01FSM7P1G0VBGAMK9D9QMGQ5MY"]), + ( + "filter[scope]=urn:mas:admin", + &["01FSPT2RG08Y11Y5BM4VZ4CN8K"], + ), + ]; + + for (filter, expected_ids) in filters_and_expected { + let request = Request::get(format!("/api/admin/v1/personal-sessions?{filter}")) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + let found: BTreeSet<&str> = body["data"] + .as_array() + .unwrap() + .iter() + .map(|item| item["id"].as_str().unwrap()) + .collect(); + let expected: BTreeSet<&str> = expected_ids.iter().copied().collect(); + + assert_eq!( + found, expected, + "filter {filter} did not produce expected results" + ); + } + } +} diff --git a/crates/handlers/src/admin/v1/personal_sessions/mod.rs b/crates/handlers/src/admin/v1/personal_sessions/mod.rs new file mode 100644 index 000000000..37c591b09 --- /dev/null +++ b/crates/handlers/src/admin/v1/personal_sessions/mod.rs @@ -0,0 +1,39 @@ +// 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. + +mod add; +mod get; +mod list; +mod regenerate; +mod revoke; + +use mas_data_model::personal::session::PersonalSessionOwner; + +pub use self::{ + add::{doc as add_doc, handler as add}, + get::{doc as get_doc, handler as get}, + list::{doc as list_doc, handler as list}, + regenerate::{doc as regenerate_doc, handler as regenerate}, + revoke::{doc as revoke_doc, handler as revoke}, +}; +use crate::admin::call_context::CallerSession; + +/// Given the [`CallerSession`] of a caller of the Admin API, +/// return the [`PersonalSessionOwner`] that should own created personal +/// sessions. +fn personal_session_owner_from_caller(caller: &CallerSession) -> PersonalSessionOwner { + match caller { + CallerSession::OAuth2Session(session) => { + if let Some(user_id) = session.user_id { + PersonalSessionOwner::User(user_id) + } else { + PersonalSessionOwner::OAuth2Client(session.client_id) + } + } + CallerSession::PersonalSession(session) => { + PersonalSessionOwner::User(session.actor_user_id) + } + } +} diff --git a/crates/handlers/src/admin/v1/personal_sessions/regenerate.rs b/crates/handlers/src/admin/v1/personal_sessions/regenerate.rs new file mode 100644 index 000000000..e6c70679f --- /dev/null +++ b/crates/handlers/src/admin/v1/personal_sessions/regenerate.rs @@ -0,0 +1,246 @@ +// 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. + +use aide::{NoApi, OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use chrono::Duration; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_data_model::{BoxRng, TokenType}; +use schemars::JsonSchema; +use serde::Deserialize; +use tracing::error; + +use crate::{ + admin::{ + call_context::CallContext, + model::{InconsistentPersonalSession, PersonalSession}, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + v1::personal_sessions::personal_session_owner_from_caller, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("User not found")] + UserNotFound, + + #[error("Session not found")] + SessionNotFound, + + #[error("Session not valid")] + SessionNotValid, + + #[error("Session does not belong to you")] + SessionNotYours, +} + +impl_from_error_for_route!(mas_storage::RepositoryError); +impl_from_error_for_route!(InconsistentPersonalSession); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::UserNotFound | Self::SessionNotFound => StatusCode::NOT_FOUND, + Self::SessionNotValid => StatusCode::UNPROCESSABLE_ENTITY, + Self::SessionNotYours => StatusCode::FORBIDDEN, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +/// # JSON payload for the `POST /api/admin/v1/personal-sessions/{id}/regenerate` endpoint +#[derive(Deserialize, JsonSchema)] +#[serde(rename = "RegeneratePersonalSessionRequest")] +pub struct Request { + /// Token expiry time in seconds. + /// If not set, the token won't expire. + expires_in: Option, +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("regeneratePersonalSession") + .summary("Regenerate a personal session by replacing its personal access token") + .tag("personal-session") + .response_with::<201, Json>, _>(|t| { + t.description( + "Personal session was regenerated and a personal access token was created", + ) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::UserNotFound); + t.description("User was not found").example(response) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.personal_sessions.add", skip_all)] +pub async fn handler( + CallContext { + mut repo, + clock, + session: caller_session, + .. + }: CallContext, + NoApi(mut rng): NoApi, + id: UlidPathParam, + Json(params): Json, +) -> Result<(StatusCode, Json>), RouteError> { + let session_id = *id; + + let session = repo + .personal_session() + .lookup(session_id) + .await? + .ok_or(RouteError::SessionNotFound)?; + + if !session.is_valid() { + // We don't revive revoked sessions through regeneration + return Err(RouteError::SessionNotValid); + } + + // If the owner is not the current caller, then currently we reject the + // regeneration. + let caller = personal_session_owner_from_caller(&caller_session); + if session.owner != caller { + return Err(RouteError::SessionNotYours); + } + + // Revoke the existing active token for the session. + let old_token_opt = repo + .personal_access_token() + .find_active_for_session(&session) + .await?; + let Some(old_token) = old_token_opt else { + // This shouldn't happen + error!("session is supposedly valid but had no access token"); + return Err(RouteError::SessionNotValid); + }; + + repo.personal_access_token() + .revoke(&clock, old_token) + .await?; + + // Create the regenerated token for the session + let access_token_string = TokenType::PersonalAccessToken.generate(&mut rng); + let access_token = repo + .personal_access_token() + .add( + &mut rng, + &clock, + &session, + &access_token_string, + params + .expires_in + .map(|exp_in| Duration::seconds(i64::from(exp_in))), + ) + .await?; + + repo.save().await?; + + Ok(( + StatusCode::CREATED, + Json(SingleResponse::new_canonical( + PersonalSession::try_from((session, Some(access_token)))? + .with_token(access_token_string), + )), + )) +} + +#[cfg(test)] +mod tests { + use chrono::Duration; + use hyper::{Request, StatusCode}; + use insta::assert_json_snapshot; + use serde_json::{Value, json}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_regenerate_personal_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + // Create a user for testing + let mut repo = state.repository().await.unwrap(); + let mut rng = state.rng(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let request = Request::post("/api/admin/v1/personal-sessions") + .bearer(&token) + .json(json!({ + "actor_user_id": user.id, + "human_name": "SuperDuperAdminCLITool Token", + "scope": "openid urn:mas:admin", + "expires_in": 3600 + })); + + let response = state.request(request).await; + response.assert_status(StatusCode::CREATED); + let created: Value = response.json(); + + let session_id = created["data"]["id"].as_str().unwrap(); + + state.clock.advance(Duration::minutes(3)); + + let request = Request::post(format!( + "/api/admin/v1/personal-sessions/{session_id}/regenerate" + )) + .bearer(&token) + .json(json!({ + "expires_in": 86400 + })); + + let response = state.request(request).await; + response.assert_status(StatusCode::CREATED); + + let body: Value = response.json(); + + assert_json_snapshot!(body, @r#" + { + "data": { + "type": "personal-session", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "revoked_at": null, + "owner_user_id": null, + "owner_client_id": "01FSHN9AG0FAQ50MT1E9FFRPZR", + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_name": "SuperDuperAdminCLITool Token", + "scope": "openid urn:mas:admin", + "last_active_at": null, + "last_active_ip": null, + "expires_at": "2022-01-17T14:43:00Z", + "access_token": "mpt_6cq7FqNSYoosbXl3bbpfh9yNy9NzuR_0vOV2O" + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG07HNEZXNQM2KNBNF6" + } + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + "#); + } +} diff --git a/crates/handlers/src/admin/v1/personal_sessions/revoke.rs b/crates/handlers/src/admin/v1/personal_sessions/revoke.rs new file mode 100644 index 000000000..10fd6650f --- /dev/null +++ b/crates/handlers/src/admin/v1/personal_sessions/revoke.rs @@ -0,0 +1,250 @@ +// 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. + +use aide::{NoApi, OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_data_model::BoxRng; +use mas_storage::queue::{QueueJobRepositoryExt as _, SyncDevicesJob}; +use ulid::Ulid; + +use crate::{ + admin::{ + call_context::CallContext, + model::{InconsistentPersonalSession, PersonalSession}, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("Personal session with ID {0} not found")] + NotFound(Ulid), + + #[error("Personal session with ID {0} is already revoked")] + AlreadyRevoked(Ulid), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); +impl_from_error_for_route!(InconsistentPersonalSession); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound(_) => StatusCode::NOT_FOUND, + Self::AlreadyRevoked(_) => StatusCode::CONFLICT, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("revokePersonalSession") + .summary("Revoke a personal session") + .tag("personal-session") + .response_with::<200, Json>, _>(|t| { + let [sample, ..] = PersonalSession::samples(); + let response = SingleResponse::new_canonical(sample); + t.description("Personal session was revoked") + .example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil())); + t.description("Personal session not found") + .example(response) + }) + .response_with::<409, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::AlreadyRevoked(Ulid::nil())); + t.description("Personal session already revoked") + .example(response) + }) +} + +#[tracing::instrument( + name = "handler.admin.v1.personal_sessions.revoke", + skip_all, + fields(personal_session.id = %*session_id), +)] +pub async fn handler( + CallContext { + mut repo, clock, .. + }: CallContext, + NoApi(mut rng): NoApi, + session_id: UlidPathParam, +) -> Result>, RouteError> { + let session_id = *session_id; + let session = repo + .personal_session() + .lookup(session_id) + .await? + .ok_or(RouteError::NotFound(session_id))?; + + if session.is_revoked() { + return Err(RouteError::AlreadyRevoked(session_id)); + } + + let session = repo.personal_session().revoke(&clock, session).await?; + + if session.has_device() { + // If the session has a device, then we are now + // deleting a device and should schedule a device sync to clean up. + repo.queue_job() + .schedule_job( + &mut rng, + &clock, + SyncDevicesJob::new_for_id(session.actor_user_id), + ) + .await?; + } + + repo.save().await?; + + Ok(Json(SingleResponse::new_canonical( + PersonalSession::try_from((session, None))?, + ))) +} + +#[cfg(test)] +mod tests { + use chrono::Duration; + use hyper::{Request, StatusCode}; + use mas_data_model::{Clock, personal::session::PersonalSessionOwner}; + use oauth2_types::scope::Scope; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_revoke_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + // Create a user and personal session for testing + let mut repo = state.repository().await.unwrap(); + let mut rng = state.rng(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + + let personal_session = repo + .personal_session() + .add( + &mut rng, + &state.clock, + PersonalSessionOwner::from(&user), + &user, + "Test session".to_owned(), + Scope::from_iter([]), + ) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let request = Request::post(format!( + "/api/admin/v1/personal-sessions/{}/revoke", + personal_session.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + // The revoked_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["revoked_at"], + serde_json::json!(Clock::now(&state.clock)) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_revoke_already_revoked_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + // Create a user and personal session for testing + let mut repo = state.repository().await.unwrap(); + let mut rng = state.rng(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + + let personal_session = repo + .personal_session() + .add( + &mut rng, + &state.clock, + PersonalSessionOwner::from(&user), + &user, + "Test session".to_owned(), + Scope::from_iter([]), + ) + .await + .unwrap(); + + // Revoke the session first + let session = repo + .personal_session() + .revoke(&state.clock, personal_session) + .await + .unwrap(); + + repo.save().await.unwrap(); + + // Move the clock forward + state.clock.advance(Duration::try_minutes(1).unwrap()); + + let request = Request::post(format!( + "/api/admin/v1/personal-sessions/{}/revoke", + session.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::CONFLICT); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + format!("Personal session with ID {} is already revoked", session.id) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_revoke_unknown_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request = + Request::post("/api/admin/v1/personal-sessions/01040G2081040G2081040G2081/revoke") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + "Personal session with ID 01040G2081040G2081040G2081 not found" + ); + } +} diff --git a/crates/handlers/src/admin/v1/policy_data/set.rs b/crates/handlers/src/admin/v1/policy_data/set.rs index e781a8996..5bee61415 100644 --- a/crates/handlers/src/admin/v1/policy_data/set.rs +++ b/crates/handlers/src/admin/v1/policy_data/set.rs @@ -59,7 +59,7 @@ fn data_example() -> serde_json::Value { #[derive(Deserialize, JsonSchema)] #[serde(rename = "SetPolicyDataRequest")] pub struct SetPolicyDataRequest { - #[schemars(example = "data_example")] + #[schemars(example = data_example())] pub data: serde_json::Value, } diff --git a/crates/handlers/src/admin/v1/site_config.rs b/crates/handlers/src/admin/v1/site_config.rs new file mode 100644 index 000000000..40a5db51a --- /dev/null +++ b/crates/handlers/src/admin/v1/site_config.rs @@ -0,0 +1,97 @@ +// 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. + +use aide::transform::TransformOperation; +use axum::{Json, extract::State}; +use schemars::JsonSchema; +use serde::Serialize; + +use crate::admin::call_context::CallContext; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, JsonSchema)] +pub struct SiteConfig { + /// The Matrix server name for which this instance is configured + server_name: String, + + /// Whether password login is enabled. + pub password_login_enabled: bool, + + /// Whether password registration is enabled. + pub password_registration_enabled: bool, + + /// Whether a valid email address is required for password registrations. + pub password_registration_email_required: bool, + + /// Whether registration tokens are required for password registrations. + pub registration_token_required: bool, + + /// Whether users can change their email. + pub email_change_allowed: bool, + + /// Whether users can change their display name. + pub displayname_change_allowed: bool, + + /// Whether users can change their password. + pub password_change_allowed: bool, + + /// Whether users can recover their account via email. + pub account_recovery_allowed: bool, + + /// Whether users can delete their own account. + pub account_deactivation_allowed: bool, + + /// Whether CAPTCHA during registration is enabled. + pub captcha_enabled: bool, + + /// Minimum password complexity, between 0 and 4. + /// This is a score from zxcvbn. + #[schemars(range(min = 0, max = 4))] + pub minimum_password_complexity: u8, +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("siteConfig") + .tag("server") + .summary("Get informations about the configuration of this MAS instance") + .response_with::<200, Json, _>(|t| { + t.example(SiteConfig { + server_name: "example.com".to_owned(), + password_login_enabled: true, + password_registration_enabled: true, + password_registration_email_required: true, + registration_token_required: true, + email_change_allowed: true, + displayname_change_allowed: true, + password_change_allowed: true, + account_recovery_allowed: true, + account_deactivation_allowed: true, + captcha_enabled: true, + minimum_password_complexity: 3, + }) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.site_config", skip_all)] +pub async fn handler( + _: CallContext, + State(site_config): State, +) -> Json { + Json(SiteConfig { + server_name: site_config.server_name, + password_login_enabled: site_config.password_login_enabled, + password_registration_enabled: site_config.password_registration_enabled, + password_registration_email_required: site_config.password_registration_email_required, + registration_token_required: site_config.registration_token_required, + email_change_allowed: site_config.email_change_allowed, + displayname_change_allowed: site_config.displayname_change_allowed, + password_change_allowed: site_config.password_change_allowed, + account_recovery_allowed: site_config.account_recovery_allowed, + account_deactivation_allowed: site_config.account_deactivation_allowed, + captcha_enabled: site_config.captcha.is_some(), + minimum_password_complexity: site_config.minimum_password_complexity, + }) +} diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs index 59efe6541..c233a9977 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs @@ -4,11 +4,8 @@ // Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; -use axum::{ - Json, - extract::{Query, rejection::QueryRejection}, - response::IntoResponse, -}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_axum_utils::record_error; @@ -21,7 +18,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UpstreamOAuthLink}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -112,16 +109,22 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let links = UpstreamOAuthLink::samples(); let pagination = mas_storage::Pagination::first(links.len()); let page = Page { - edges: links.into(), + edges: links + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; t.description("Paginated response of upstream OAuth 2.0 links") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UpstreamOAuthLink::PATH, )) }) @@ -135,10 +138,11 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { #[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UpstreamOAuthLink::PATH); + let base = include_count.add_to_base(&base); let filter = UpstreamOAuthLinkFilter::default(); // Load the user from the filter @@ -183,15 +187,31 @@ pub async fn handler( filter }; - let page = repo.upstream_oauth_link().list(filter, pagination).await?; - let count = repo.upstream_oauth_link().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .upstream_oauth_link() + .list(filter, pagination) + .await? + .map(UpstreamOAuthLink::from); + let count = repo.upstream_oauth_link().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .upstream_oauth_link() + .list(filter, pagination) + .await? + .map(UpstreamOAuthLink::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.upstream_oauth_link().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(UpstreamOAuthLink::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] @@ -296,7 +316,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 3 @@ -314,6 +334,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } }, { @@ -328,6 +353,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4" + } } }, { @@ -342,6 +372,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07" + } } } ], @@ -351,7 +386,7 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?page[last]=10" } } - "###); + "#); // Filter by user ID let request = Request::get(format!( @@ -364,7 +399,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -382,6 +417,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } }, { @@ -396,6 +436,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07" + } } } ], @@ -405,7 +450,7 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10" } } - "###); + "#); // Filter by provider let request = Request::get(format!( @@ -418,7 +463,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -436,6 +481,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } }, { @@ -450,6 +500,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4" + } } } ], @@ -459,7 +514,7 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?filter[provider]=01FSHN9AG09NMZYX8MFYH578R9&page[last]=10" } } - "###); + "#); // Filter by subject let request = Request::get(format!( @@ -472,7 +527,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -490,6 +545,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } } } ], @@ -499,6 +559,181 @@ mod tests { "last": "/api/admin/v1/upstream-oauth-links?filter[subject]=subject1&page[last]=10" } } + "#); + + // Test count=false + let request = Request::get("/api/admin/v1/upstream-oauth-links?count=false") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "upstream-oauth-link", + "id": "01FSHN9AG0AQZQP8DX40GD59PW", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "provider_id": "01FSHN9AG09NMZYX8MFYH578R9", + "subject": "subject1", + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_account_name": "alice@acme" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } + } + }, + { + "type": "upstream-oauth-link", + "id": "01FSHN9AG0PJZ6DZNTAA1XKPT4", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "provider_id": "01FSHN9AG09NMZYX8MFYH578R9", + "subject": "subject3", + "user_id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "human_account_name": "bob@acme" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4" + } + } + }, + { + "type": "upstream-oauth-link", + "id": "01FSHN9AG0QHEHKX2JNQ2A2D07", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z", + "subject": "subject2", + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_account_name": "alice@example" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-links?count=false&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-links?count=false&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-links?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/upstream-oauth-links?count=only") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r###" + { + "meta": { + "count": 3 + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-links?count=only" + } + } "###); + + // Test count=false with filtering + let request = Request::get(format!( + "/api/admin/v1/upstream-oauth-links?count=false&filter[user]={}", + alice.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "upstream-oauth-link", + "id": "01FSHN9AG0AQZQP8DX40GD59PW", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "provider_id": "01FSHN9AG09NMZYX8MFYH578R9", + "subject": "subject1", + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_account_name": "alice@acme" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AQZQP8DX40GD59PW" + } + } + }, + { + "type": "upstream-oauth-link", + "id": "01FSHN9AG0QHEHKX2JNQ2A2D07", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z", + "subject": "subject2", + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_account_name": "alice@example" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[last]=10" + } + } + "#); + + // Test count=only with filtering + let request = Request::get(format!( + "/api/admin/v1/upstream-oauth-links?count=only&filter[provider]={}", + provider1.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 2 + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-links?filter[provider]=01FSHN9AG09NMZYX8MFYH578R9&count=only" + } + } + "#); } } diff --git a/crates/handlers/src/admin/v1/upstream_oauth_providers/get.rs b/crates/handlers/src/admin/v1/upstream_oauth_providers/get.rs new file mode 100644 index 000000000..3700e1a65 --- /dev/null +++ b/crates/handlers/src/admin/v1/upstream_oauth_providers/get.rs @@ -0,0 +1,196 @@ +// 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. + +use aide::{OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_storage::{RepositoryAccess, upstream_oauth2::UpstreamOAuthProviderRepository}; + +use crate::{ + admin::{ + call_context::CallContext, + model::UpstreamOAuthProvider, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("Provider not found")] + NotFound, +} + +impl_from_error_for_route!(mas_storage::RepositoryError); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound => StatusCode::NOT_FOUND, + }; + + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("getUpstreamOAuthProvider") + .summary("Get upstream OAuth provider") + .tag("upstream-oauth-provider") + .response_with::<200, Json>, _>(|t| { + let [sample, ..] = UpstreamOAuthProvider::samples(); + t.description("The upstream OAuth provider") + .example(SingleResponse::new_canonical(sample)) + }) + .response_with::<404, Json, _>(|t| t.description("Provider not found")) +} + +#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_providers.get", skip_all)] +pub async fn handler( + CallContext { mut repo, .. }: CallContext, + id: UlidPathParam, +) -> Result>, RouteError> { + let provider = repo + .upstream_oauth_provider() + .lookup(*id) + .await? + .ok_or(RouteError::NotFound)?; + + Ok(Json(SingleResponse::new_canonical( + UpstreamOAuthProvider::from(provider), + ))) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use mas_data_model::{ + UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports, + UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderOnBackchannelLogout, + UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod, + }; + use mas_iana::jose::JsonWebSignatureAlg; + use mas_storage::{ + RepositoryAccess, + upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository}, + }; + use oauth2_types::scope::{OPENID, Scope}; + use sqlx::PgPool; + use ulid::Ulid; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + async fn create_test_provider(state: &mut TestState) -> UpstreamOAuthProvider { + let mut repo = state.repository().await.unwrap(); + + let params = UpstreamOAuthProviderParams { + issuer: Some("https://accounts.google.com".to_owned()), + human_name: Some("Google".to_owned()), + brand_name: Some("google".to_owned()), + discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: UpstreamOAuthProviderPkceMode::Auto, + jwks_uri_override: None, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: true, + userinfo_signed_response_alg: None, + client_id: "google-client-id".to_owned(), + encrypted_client_secret: Some("encrypted-secret".to_owned()), + token_endpoint_signing_alg: None, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + response_mode: None, + scope: Scope::from_iter([OPENID]), + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + additional_authorization_parameters: vec![], + forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }; + + let provider = repo + .upstream_oauth_provider() + .add(&mut state.rng(), &state.clock, params) + .await + .unwrap(); + + Box::new(repo).save().await.unwrap(); + + provider + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_get_provider(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + let provider = create_test_provider(&mut state).await; + + let request = Request::get(format!( + "/api/admin/v1/upstream-oauth-providers/{}", + provider.id + )) + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + assert_eq!(body["data"]["type"], "upstream-oauth-provider"); + assert_eq!(body["data"]["id"], provider.id.to_string()); + assert_eq!(body["data"]["attributes"]["human_name"], "Google"); + + insta::assert_json_snapshot!(body, @r###" + { + "data": { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + } + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + "###); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_not_found(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + + let provider_id = Ulid::nil(); + let request = Request::get(format!( + "/api/admin/v1/upstream-oauth-providers/{provider_id}" + )) + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + } +} diff --git a/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs new file mode 100644 index 000000000..d70bbd299 --- /dev/null +++ b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs @@ -0,0 +1,799 @@ +// 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. + +use aide::{OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; +use axum_macros::FromRequestParts; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_storage::{Page, upstream_oauth2::UpstreamOAuthProviderFilter}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::{ + admin::{ + call_context::CallContext, + model::{Resource, UpstreamOAuthProvider}, + params::{IncludeCount, Pagination}, + response::{ErrorResponse, PaginatedResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(FromRequestParts, Deserialize, JsonSchema, OperationIo)] +#[serde(rename = "UpstreamOAuthProviderFilter")] +#[aide(input_with = "Query")] +#[from_request(via(Query), rejection(RouteError))] +pub struct FilterParams { + /// Retrieve providers that are (or are not) enabled + #[serde(rename = "filter[enabled]")] + enabled: Option, +} + +impl std::fmt::Display for FilterParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut sep = '?'; + + if let Some(enabled) = self.enabled { + write!(f, "{sep}filter[enabled]={enabled}")?; + sep = '&'; + } + + let _ = sep; + Ok(()) + } +} + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("Invalid filter parameters")] + InvalidFilter(#[from] QueryRejection), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, + }; + + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("listUpstreamOAuthProviders") + .summary("List upstream OAuth 2.0 providers") + .tag("upstream-oauth-provider") + .response_with::<200, Json>, _>(|t| { + let providers = UpstreamOAuthProvider::samples(); + let pagination = mas_storage::Pagination::first(providers.len()); + let page = Page { + edges: providers + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), + has_next_page: true, + has_previous_page: false, + }; + + t.description("Paginated response of upstream OAuth 2.0 providers") + .example(PaginatedResponse::for_page( + page, + pagination, + Some(42), + UpstreamOAuthProvider::PATH, + )) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_providers.list", skip_all)] +pub async fn handler( + CallContext { mut repo, .. }: CallContext, + Pagination(pagination, include_count): Pagination, + params: FilterParams, +) -> Result>, RouteError> { + let base = format!("{path}{params}", path = UpstreamOAuthProvider::PATH); + let base = include_count.add_to_base(&base); + let filter = UpstreamOAuthProviderFilter::new(); + + let filter = match params.enabled { + Some(true) => filter.enabled_only(), + Some(false) => filter.disabled_only(), + None => filter, + }; + + let response = match include_count { + IncludeCount::True => { + let page = repo + .upstream_oauth_provider() + .list(filter, pagination) + .await? + .map(UpstreamOAuthProvider::from); + let count = repo.upstream_oauth_provider().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .upstream_oauth_provider() + .list(filter, pagination) + .await? + .map(UpstreamOAuthProvider::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.upstream_oauth_provider().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; + + Ok(Json(response)) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use mas_data_model::{ + UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode, + UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode, + UpstreamOAuthProviderTokenAuthMethod, + }; + use mas_iana::jose::JsonWebSignatureAlg; + use mas_storage::{ + RepositoryAccess, + upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository}, + }; + use oauth2_types::scope::{OPENID, Scope}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + async fn create_test_providers(state: &mut TestState) { + let mut repo = state.repository().await.unwrap(); + + // Create an enabled provider + let enabled_params = UpstreamOAuthProviderParams { + issuer: Some("https://accounts.google.com".to_owned()), + human_name: Some("Google".to_owned()), + brand_name: Some("google".to_owned()), + discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: UpstreamOAuthProviderPkceMode::Auto, + jwks_uri_override: None, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: true, + userinfo_signed_response_alg: None, + client_id: "google-client-id".to_owned(), + encrypted_client_secret: Some("encrypted-secret".to_owned()), + token_endpoint_signing_alg: None, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + response_mode: None, + scope: Scope::from_iter([OPENID]), + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + additional_authorization_parameters: vec![], + forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }; + + repo.upstream_oauth_provider() + .add(&mut state.rng(), &state.clock, enabled_params) + .await + .unwrap(); + + // Create a disabled provider + let disabled_params = UpstreamOAuthProviderParams { + issuer: Some("https://appleid.apple.com".to_owned()), + human_name: Some("Apple ID".to_owned()), + brand_name: Some("apple".to_owned()), + discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: UpstreamOAuthProviderPkceMode::S256, + jwks_uri_override: None, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: true, + userinfo_signed_response_alg: None, + client_id: "apple-client-id".to_owned(), + encrypted_client_secret: Some("encrypted-secret".to_owned()), + token_endpoint_signing_alg: None, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + response_mode: None, + scope: Scope::from_iter([OPENID]), + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + additional_authorization_parameters: vec![], + forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 1, + }; + + let disabled_provider = repo + .upstream_oauth_provider() + .add(&mut state.rng(), &state.clock, disabled_params) + .await + .unwrap(); + + // Disable the provider + repo.upstream_oauth_provider() + .disable(&state.clock, disabled_provider) + .await + .unwrap(); + + // Create another enabled provider + let another_enabled_params = UpstreamOAuthProviderParams { + issuer: Some("https://login.microsoftonline.com/common/v2.0".to_owned()), + human_name: Some("Microsoft".to_owned()), + brand_name: Some("microsoft".to_owned()), + discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: UpstreamOAuthProviderPkceMode::Auto, + jwks_uri_override: None, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: true, + userinfo_signed_response_alg: None, + client_id: "microsoft-client-id".to_owned(), + encrypted_client_secret: Some("encrypted-secret".to_owned()), + token_endpoint_signing_alg: None, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + response_mode: None, + scope: Scope::from_iter([OPENID]), + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + additional_authorization_parameters: vec![], + forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 2, + }; + + repo.upstream_oauth_provider() + .add(&mut state.rng(), &state.clock, another_enabled_params) + .await + .unwrap(); + + Box::new(repo).save().await.unwrap(); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_list_all_providers(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + let request = Request::get("/api/admin/v1/upstream-oauth-providers") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + // Should return all providers + assert_eq!(body["data"].as_array().unwrap().len(), 3); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=10" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_filter_by_enabled_true(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + let request = Request::get("/api/admin/v1/upstream-oauth-providers?filter[enabled]=true") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 2 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&page[last]=10" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_filter_by_enabled_false(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + let request = Request::get("/api/admin/v1/upstream-oauth-providers?filter[enabled]=false") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=false&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=false&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=false&page[last]=10" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_pagination(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + // Test first page with limit of 2 + let request = Request::get("/api/admin/v1/upstream-oauth-providers?page[first]=2") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[first]=2", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=2", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=2", + "next": "/api/admin/v1/upstream-oauth-providers?page[after]=01FSHN9AG09AVTNSQFMSR34AJC&page[first]=2" + } + } + "#); + + // Extract the ID of the last item for pagination + let last_item_id = body["data"][1]["id"].as_str().unwrap(); + let request = Request::get(format!( + "/api/admin/v1/upstream-oauth-providers?page[first]=2&page[after]={last_item_id}", + )) + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[after]=01FSHN9AG09AVTNSQFMSR34AJC&page[first]=2", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=2", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=2" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_invalid_filter(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + + let request = + Request::get("/api/admin/v1/upstream-oauth-providers?filter[enabled]=invalid") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::BAD_REQUEST); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_count_parameter(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + // Test count=false + let request = Request::get("/api/admin/v1/upstream-oauth-providers?count=false") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?count=false&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?count=false&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/upstream-oauth-providers?count=only") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?count=only" + } + } + "#); + + // Test count=false with filtering + let request = + Request::get("/api/admin/v1/upstream-oauth-providers?count=false&filter[enabled]=true") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&count=false&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&count=false&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&count=false&page[last]=10" + } + } + "#); + + // Test count=only with filtering + let request = + Request::get("/api/admin/v1/upstream-oauth-providers?count=only&filter[enabled]=false") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=false&count=only" + } + } + "#); + } +} diff --git a/crates/handlers/src/admin/v1/upstream_oauth_providers/mod.rs b/crates/handlers/src/admin/v1/upstream_oauth_providers/mod.rs new file mode 100644 index 000000000..18ffe5af6 --- /dev/null +++ b/crates/handlers/src/admin/v1/upstream_oauth_providers/mod.rs @@ -0,0 +1,12 @@ +// 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. + +mod get; +mod list; + +pub use self::{ + get::{doc as get_doc, handler as get}, + list::{doc as list_doc, handler as list}, +}; diff --git a/crates/handlers/src/admin/v1/user_emails/list.rs b/crates/handlers/src/admin/v1/user_emails/list.rs index 92dfe12c2..453ef0e89 100644 --- a/crates/handlers/src/admin/v1/user_emails/list.rs +++ b/crates/handlers/src/admin/v1/user_emails/list.rs @@ -4,11 +4,8 @@ // Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; -use axum::{ - Json, - extract::{Query, rejection::QueryRejection}, - response::IntoResponse, -}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_axum_utils::record_error; @@ -21,7 +18,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UserEmail}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -99,16 +96,22 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let emails = UserEmail::samples(); let pagination = mas_storage::Pagination::first(emails.len()); let page = Page { - edges: emails.into(), + edges: emails + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; t.description("Paginated response of user emails") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UserEmail::PATH, )) }) @@ -121,10 +124,11 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { #[tracing::instrument(name = "handler.admin.v1.user_emails.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UserEmail::PATH); + let base = include_count.add_to_base(&base); let filter = UserEmailFilter::default(); // Load the user from the filter @@ -150,15 +154,31 @@ pub async fn handler( None => filter, }; - let page = repo.user_email().list(filter, pagination).await?; - let count = repo.user_email().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .user_email() + .list(filter, pagination) + .await? + .map(UserEmail::from); + let count = repo.user_email().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .user_email() + .list(filter, pagination) + .await? + .map(UserEmail::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.user_email().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(UserEmail::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] @@ -209,7 +229,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -225,6 +245,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } } }, { @@ -237,6 +262,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG0KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0KEPHYQQXW9XPTX6Z" + } } } ], @@ -246,7 +276,7 @@ mod tests { "last": "/api/admin/v1/user-emails?page[last]=10" } } - "###); + "#); // Filter by user let request = Request::get(format!( @@ -258,7 +288,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -274,6 +304,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } } } ], @@ -283,7 +318,7 @@ mod tests { "last": "/api/admin/v1/user-emails?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10" } } - "###); + "#); // Filter by email let request = Request::get("/api/admin/v1/user-emails?filter[email]=alice@example.com") @@ -292,7 +327,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - insta::assert_json_snapshot!(body, @r###" + insta::assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -308,6 +343,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } } } ], @@ -317,6 +357,137 @@ mod tests { "last": "/api/admin/v1/user-emails?filter[email]=alice@example.com&page[last]=10" } } + "#); + + // Test count=false + let request = Request::get("/api/admin/v1/user-emails?count=false") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user-email", + "id": "01FSHN9AG09NMZYX8MFYH578R9", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "email": "alice@example.com" + }, + "links": { + "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } + } + }, + { + "type": "user-email", + "id": "01FSHN9AG0KEPHYQQXW9XPTX6Z", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "user_id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "email": "bob@example.com" + }, + "links": { + "self": "/api/admin/v1/user-emails/01FSHN9AG0KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0KEPHYQQXW9XPTX6Z" + } + } + } + ], + "links": { + "self": "/api/admin/v1/user-emails?count=false&page[first]=10", + "first": "/api/admin/v1/user-emails?count=false&page[first]=10", + "last": "/api/admin/v1/user-emails?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/user-emails?count=only") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "meta": { + "count": 2 + }, + "links": { + "self": "/api/admin/v1/user-emails?count=only" + } + } "###); + + // Test count=false with filtering + let request = Request::get(format!( + "/api/admin/v1/user-emails?count=false&filter[user]={}", + alice.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user-email", + "id": "01FSHN9AG09NMZYX8MFYH578R9", + "attributes": { + "created_at": "2022-01-16T14:40:00Z", + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "email": "alice@example.com" + }, + "links": { + "self": "/api/admin/v1/user-emails/01FSHN9AG09NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09NMZYX8MFYH578R9" + } + } + } + ], + "links": { + "self": "/api/admin/v1/user-emails?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "first": "/api/admin/v1/user-emails?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "last": "/api/admin/v1/user-emails?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[last]=10" + } + } + "#); + + // Test count=only with filtering + let request = Request::get(format!( + "/api/admin/v1/user-emails?count=only&filter[user]={}", + alice.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "links": { + "self": "/api/admin/v1/user-emails?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=only" + } + } + "#); } } diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs index 546491536..26e925401 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs @@ -5,11 +5,8 @@ // Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; -use axum::{ - Json, - extract::{Query, rejection::QueryRejection}, - response::IntoResponse, -}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_axum_utils::record_error; @@ -21,7 +18,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UserRegistrationToken}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -112,16 +109,22 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let tokens = UserRegistrationToken::samples(); let pagination = mas_storage::Pagination::first(tokens.len()); let page = Page { - edges: tokens.into(), + edges: tokens + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; t.description("Paginated response of registration tokens") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UserRegistrationToken::PATH, )) }) @@ -132,10 +135,11 @@ pub async fn handler( CallContext { mut repo, clock, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UserRegistrationToken::PATH); + let base = include_count.add_to_base(&base); let now = clock.now(); let mut filter = UserRegistrationTokenFilter::new(now); @@ -155,18 +159,31 @@ pub async fn handler( filter = filter.with_valid(valid); } - let page = repo - .user_registration_token() - .list(filter, pagination) - .await?; - let count = repo.user_registration_token().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .user_registration_token() + .list(filter, pagination) + .await? + .map(|token| UserRegistrationToken::new(token, now)); + let count = repo.user_registration_token().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .user_registration_token() + .list(filter, pagination) + .await? + .map(|token| UserRegistrationToken::new(token, now)); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.user_registration_token().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(|token| UserRegistrationToken::new(token, now)), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] @@ -300,6 +317,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -317,6 +339,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -334,6 +361,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -351,6 +383,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } }, { @@ -368,6 +405,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -416,6 +458,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -433,6 +480,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -473,6 +525,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -490,6 +547,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -507,6 +569,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -555,6 +622,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -572,6 +644,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -612,6 +689,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -629,6 +711,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -646,6 +733,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -694,6 +786,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } } ], @@ -734,6 +831,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -751,6 +853,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -768,6 +875,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } }, { @@ -785,6 +897,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -833,6 +950,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } }, { @@ -850,6 +972,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -890,6 +1017,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -907,6 +1039,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -924,6 +1061,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -974,6 +1116,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -1022,6 +1169,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } } }, { @@ -1039,6 +1191,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } } } ], @@ -1080,6 +1237,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } } }, { @@ -1097,6 +1259,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -1138,6 +1305,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } } } ], @@ -1172,4 +1344,242 @@ mod tests { .contains("Invalid filter parameters") ); } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_count_parameter(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_tokens(&mut state).await; + + // Test count=false + let request = Request::get("/api/admin/v1/user-registration-tokens?count=false") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user-registration_token", + "id": "01FSHN9AG064K8BYZXSY5G511Z", + "attributes": { + "token": "token_expired", + "valid": false, + "usage_limit": 5, + "times_used": 0, + "created_at": "2022-01-16T14:40:00Z", + "last_used_at": null, + "expires_at": "2022-01-15T14:40:00Z", + "revoked_at": null + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG064K8BYZXSY5G511Z" + } + } + }, + { + "type": "user-registration_token", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "token": "token_used", + "valid": true, + "usage_limit": 10, + "times_used": 1, + "created_at": "2022-01-16T14:40:00Z", + "last_used_at": "2022-01-16T14:40:00Z", + "expires_at": null, + "revoked_at": null + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + }, + { + "type": "user-registration_token", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "token": "token_revoked", + "valid": false, + "usage_limit": 10, + "times_used": 0, + "created_at": "2022-01-16T14:40:00Z", + "last_used_at": null, + "expires_at": null, + "revoked_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG09AVTNSQFMSR34AJC" + } + } + }, + { + "type": "user-registration_token", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "token": "token_unused", + "valid": true, + "usage_limit": 10, + "times_used": 0, + "created_at": "2022-01-16T14:40:00Z", + "last_used_at": null, + "expires_at": null, + "revoked_at": null + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + }, + { + "type": "user-registration_token", + "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN", + "attributes": { + "token": "token_used_revoked", + "valid": false, + "usage_limit": 10, + "times_used": 1, + "created_at": "2022-01-16T14:40:00Z", + "last_used_at": "2022-01-16T14:40:00Z", + "expires_at": null, + "revoked_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0S3ZJD8CXQ7F11KXN" + } + } + } + ], + "links": { + "self": "/api/admin/v1/user-registration-tokens?count=false&page[first]=10", + "first": "/api/admin/v1/user-registration-tokens?count=false&page[first]=10", + "last": "/api/admin/v1/user-registration-tokens?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/user-registration-tokens?count=only") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 5 + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens?count=only" + } + } + "#); + + // Test count=false with filtering + let request = + Request::get("/api/admin/v1/user-registration-tokens?count=false&filter[valid]=true") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user-registration_token", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "token": "token_used", + "valid": true, + "usage_limit": 10, + "times_used": 1, + "created_at": "2022-01-16T14:40:00Z", + "last_used_at": "2022-01-16T14:40:00Z", + "expires_at": null, + "revoked_at": null + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + }, + { + "type": "user-registration_token", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "token": "token_unused", + "valid": true, + "usage_limit": 10, + "times_used": 0, + "created_at": "2022-01-16T14:40:00Z", + "last_used_at": null, + "expires_at": null, + "revoked_at": null + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/user-registration-tokens?filter[valid]=true&count=false&page[first]=10", + "first": "/api/admin/v1/user-registration-tokens?filter[valid]=true&count=false&page[first]=10", + "last": "/api/admin/v1/user-registration-tokens?filter[valid]=true&count=false&page[last]=10" + } + } + "#); + + // Test count=only with filtering + let request = + Request::get("/api/admin/v1/user-registration-tokens?count=only&filter[revoked]=true") + .bearer(&admin_token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 2 + }, + "links": { + "self": "/api/admin/v1/user-registration-tokens?filter[revoked]=true&count=only" + } + } + "#); + } } diff --git a/crates/handlers/src/admin/v1/user_sessions/finish.rs b/crates/handlers/src/admin/v1/user_sessions/finish.rs new file mode 100644 index 000000000..a50253f11 --- /dev/null +++ b/crates/handlers/src/admin/v1/user_sessions/finish.rs @@ -0,0 +1,216 @@ +// 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. + +use aide::{OperationIo, transform::TransformOperation}; +use axum::{Json, response::IntoResponse}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use ulid::Ulid; + +use crate::{ + admin::{ + call_context::CallContext, + model::{Resource, UserSession}, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("User session with ID {0} not found")] + NotFound(Ulid), + + #[error("User session with ID {0} is already finished")] + AlreadyFinished(Ulid), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound(_) => StatusCode::NOT_FOUND, + Self::AlreadyFinished(_) => StatusCode::BAD_REQUEST, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("finishUserSession") + .summary("Finish a user session") + .description( + "Calling this endpoint will finish the user session, preventing any further use.", + ) + .tag("user-session") + .response_with::<200, Json>, _>(|t| { + // Get the finished session sample + let [_, _, finished_session] = UserSession::samples(); + let id = finished_session.id(); + let response = SingleResponse::new( + finished_session, + format!("/api/admin/v1/user-sessions/{id}/finish"), + ); + t.description("User session was finished").example(response) + }) + .response_with::<400, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::AlreadyFinished(Ulid::nil())); + t.description("Session is already finished") + .example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil())); + t.description("User session was not found") + .example(response) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.user_sessions.finish", skip_all)] +pub async fn handler( + CallContext { + mut repo, clock, .. + }: CallContext, + id: UlidPathParam, +) -> Result>, RouteError> { + let id = *id; + let session = repo + .browser_session() + .lookup(id) + .await? + .ok_or(RouteError::NotFound(id))?; + + // Check if the session is already finished + if session.finished_at.is_some() { + return Err(RouteError::AlreadyFinished(id)); + } + + // Finish the session + let session = repo.browser_session().finish(&clock, session).await?; + + repo.save().await?; + + Ok(Json(SingleResponse::new( + UserSession::from(session), + format!("/api/admin/v1/user-sessions/{id}/finish"), + ))) +} + +#[cfg(test)] +mod tests { + use chrono::Duration; + use hyper::{Request, StatusCode}; + use mas_data_model::Clock as _; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + let mut rng = state.rng(); + + // Provision a user and a user session + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let request = Request::post(format!("/api/admin/v1/user-sessions/{}/finish", session.id)) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + // The finished_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["finished_at"], + serde_json::json!(state.clock.now()) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_already_finished_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + let mut rng = state.rng(); + + // Provision a user and a user session + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + + // Finish the session first + let session = repo + .browser_session() + .finish(&state.clock, session) + .await + .unwrap(); + + repo.save().await.unwrap(); + + // Move the clock forward + state.clock.advance(Duration::try_minutes(1).unwrap()); + + let request = Request::post(format!("/api/admin/v1/user-sessions/{}/finish", session.id)) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::BAD_REQUEST); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + format!("User session with ID {} is already finished", session.id) + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_finish_unknown_session(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request = + Request::post("/api/admin/v1/user-sessions/01040G2081040G2081040G2081/finish") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + "User session with ID 01040G2081040G2081040G2081 not found" + ); + } +} diff --git a/crates/handlers/src/admin/v1/user_sessions/list.rs b/crates/handlers/src/admin/v1/user_sessions/list.rs index 28a52edf2..ad8a05982 100644 --- a/crates/handlers/src/admin/v1/user_sessions/list.rs +++ b/crates/handlers/src/admin/v1/user_sessions/list.rs @@ -4,11 +4,8 @@ // Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; -use axum::{ - Json, - extract::{Query, rejection::QueryRejection}, - response::IntoResponse, -}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_axum_utils::record_error; @@ -21,7 +18,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UserSession}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -123,16 +120,22 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p let sessions = UserSession::samples(); let pagination = mas_storage::Pagination::first(sessions.len()); let page = Page { - edges: sessions.into(), + edges: sessions + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; t.description("Paginated response of user sessions") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UserSession::PATH, )) }) @@ -145,10 +148,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p #[tracing::instrument(name = "handler.admin.v1.user_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UserSession::PATH); + let base = include_count.add_to_base(&base); let filter = BrowserSessionFilter::default(); // Load the user from the filter @@ -175,15 +179,31 @@ pub async fn handler( None => filter, }; - let page = repo.browser_session().list(filter, pagination).await?; - let count = repo.browser_session().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .browser_session() + .list(filter, pagination) + .await? + .map(UserSession::from); + let count = repo.browser_session().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .browser_session() + .list(filter, pagination) + .await? + .map(UserSession::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.browser_session().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(UserSession::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] @@ -241,7 +261,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 2 @@ -260,6 +280,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } }, { @@ -275,6 +300,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB530KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AJ6AC5HQ9X6H4RP4" + } } } ], @@ -284,7 +314,7 @@ mod tests { "last": "/api/admin/v1/user-sessions?page[last]=10" } } - "###); + "#); // Filter by user let request = Request::get(format!( @@ -296,7 +326,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -315,6 +345,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -324,7 +359,7 @@ mod tests { "last": "/api/admin/v1/user-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10" } } - "###); + "#); // Filter by status (active) let request = Request::get("/api/admin/v1/user-sessions?filter[status]=active") @@ -333,7 +368,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -352,6 +387,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } } } ], @@ -361,7 +401,7 @@ mod tests { "last": "/api/admin/v1/user-sessions?filter[status]=active&page[last]=10" } } - "###); + "#); // Filter by status (finished) let request = Request::get("/api/admin/v1/user-sessions?filter[status]=finished") @@ -370,7 +410,7 @@ mod tests { let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r###" + assert_json_snapshot!(body, @r#" { "meta": { "count": 1 @@ -389,6 +429,11 @@ mod tests { }, "links": { "self": "/api/admin/v1/user-sessions/01FSHNB530KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AJ6AC5HQ9X6H4RP4" + } } } ], @@ -398,6 +443,143 @@ mod tests { "last": "/api/admin/v1/user-sessions?filter[status]=finished&page[last]=10" } } + "#); + + // Test count=false + let request = Request::get("/api/admin/v1/user-sessions?count=false") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user-session", + "id": "01FSHNB5309NMZYX8MFYH578R9", + "attributes": { + "created_at": "2022-01-16T14:41:00Z", + "finished_at": null, + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "user_agent": null, + "last_active_at": null, + "last_active_ip": null + }, + "links": { + "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + }, + { + "type": "user-session", + "id": "01FSHNB530KEPHYQQXW9XPTX6Z", + "attributes": { + "created_at": "2022-01-16T14:41:00Z", + "finished_at": "2022-01-16T14:42:00Z", + "user_id": "01FSHNB530AJ6AC5HQ9X6H4RP4", + "user_agent": null, + "last_active_at": null, + "last_active_ip": null + }, + "links": { + "self": "/api/admin/v1/user-sessions/01FSHNB530KEPHYQQXW9XPTX6Z" + }, + "meta": { + "page": { + "cursor": "01FSHNB530AJ6AC5HQ9X6H4RP4" + } + } + } + ], + "links": { + "self": "/api/admin/v1/user-sessions?count=false&page[first]=10", + "first": "/api/admin/v1/user-sessions?count=false&page[first]=10", + "last": "/api/admin/v1/user-sessions?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/user-sessions?count=only") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r###" + { + "meta": { + "count": 2 + }, + "links": { + "self": "/api/admin/v1/user-sessions?count=only" + } + } "###); + + // Test count=false with filtering + let request = Request::get(format!( + "/api/admin/v1/user-sessions?count=false&filter[user]={}", + alice.id + )) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user-session", + "id": "01FSHNB5309NMZYX8MFYH578R9", + "attributes": { + "created_at": "2022-01-16T14:41:00Z", + "finished_at": null, + "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "user_agent": null, + "last_active_at": null, + "last_active_ip": null + }, + "links": { + "self": "/api/admin/v1/user-sessions/01FSHNB5309NMZYX8MFYH578R9" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/user-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "first": "/api/admin/v1/user-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10", + "last": "/api/admin/v1/user-sessions?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[last]=10" + } + } + "#); + + // Test count=only with filtering + let request = Request::get("/api/admin/v1/user-sessions?count=only&filter[status]=active") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "links": { + "self": "/api/admin/v1/user-sessions?filter[status]=active&count=only" + } + } + "#); } } diff --git a/crates/handlers/src/admin/v1/user_sessions/mod.rs b/crates/handlers/src/admin/v1/user_sessions/mod.rs index 18ffe5af6..db7b17ff5 100644 --- a/crates/handlers/src/admin/v1/user_sessions/mod.rs +++ b/crates/handlers/src/admin/v1/user_sessions/mod.rs @@ -3,10 +3,12 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +mod finish; mod get; mod list; pub use self::{ + finish::{doc as finish_doc, handler as finish}, get::{doc as get_doc, handler as get}, list::{doc as list_doc, handler as list}, }; diff --git a/crates/handlers/src/admin/v1/users/deactivate.rs b/crates/handlers/src/admin/v1/users/deactivate.rs index 8a135fadc..b963b73d5 100644 --- a/crates/handlers/src/admin/v1/users/deactivate.rs +++ b/crates/handlers/src/admin/v1/users/deactivate.rs @@ -209,7 +209,8 @@ mod tests { "created_at": "2022-01-16T14:40:00Z", "locked_at": null, "deactivated_at": "2022-01-16T14:40:00Z", - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" @@ -289,7 +290,8 @@ mod tests { "created_at": "2022-01-16T14:40:00Z", "locked_at": "2022-01-16T14:40:00Z", "deactivated_at": "2022-01-16T14:41:00Z", - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" diff --git a/crates/handlers/src/admin/v1/users/list.rs b/crates/handlers/src/admin/v1/users/list.rs index 021e39f37..65375402e 100644 --- a/crates/handlers/src/admin/v1/users/list.rs +++ b/crates/handlers/src/admin/v1/users/list.rs @@ -5,11 +5,8 @@ // Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; -use axum::{ - Json, - extract::{Query, rejection::QueryRejection}, - response::IntoResponse, -}; +use axum::{Json, response::IntoResponse}; +use axum_extra::extract::{Query, QueryRejection}; use axum_macros::FromRequestParts; use hyper::StatusCode; use mas_axum_utils::record_error; @@ -21,7 +18,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, User}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -54,6 +51,17 @@ pub struct FilterParams { #[serde(rename = "filter[admin]")] admin: Option, + /// Retrieve users with (or without) the `legacy_guest` flag set + #[serde(rename = "filter[legacy-guest]")] + legacy_guest: Option, + + /// Retrieve users where the username matches contains the given string + /// + /// Note that this doesn't change the ordering of the result, which are + /// still ordered by ID. + #[serde(rename = "filter[search]")] + search: Option, + /// Retrieve the items with the given status /// /// Defaults to retrieve all users, including locked ones. @@ -75,6 +83,14 @@ impl std::fmt::Display for FilterParams { write!(f, "{sep}filter[admin]={admin}")?; sep = '&'; } + if let Some(legacy_guest) = self.legacy_guest { + write!(f, "{sep}filter[legacy-guest]={legacy_guest}")?; + sep = '&'; + } + if let Some(search) = &self.search { + write!(f, "{sep}filter[search]={search}")?; + sep = '&'; + } if let Some(status) = self.status { write!(f, "{sep}filter[status]={status}")?; sep = '&'; @@ -118,23 +134,35 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { let users = User::samples(); let pagination = mas_storage::Pagination::first(users.len()); let page = Page { - edges: users.into(), + edges: users + .into_iter() + .map(|node| mas_storage::pagination::Edge { + cursor: node.id(), + node, + }) + .collect(), has_next_page: true, has_previous_page: false, }; t.description("Paginated response of users") - .example(PaginatedResponse::new(page, pagination, 42, User::PATH)) + .example(PaginatedResponse::for_page( + page, + pagination, + Some(42), + User::PATH, + )) }) } #[tracing::instrument(name = "handler.admin.v1.users.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = User::PATH); + let base = include_count.add_to_base(&base); let filter = UserFilter::default(); let filter = match params.admin { @@ -143,6 +171,17 @@ pub async fn handler( None => filter, }; + let filter = match params.legacy_guest { + Some(true) => filter.guest_only(), + Some(false) => filter.non_guest_only(), + None => filter, + }; + + let filter = match params.search.as_deref() { + Some(search) => filter.matching_search(search), + None => filter, + }; + let filter = match params.status { Some(UserStatus::Active) => filter.active_only(), Some(UserStatus::Locked) => filter.locked_only(), @@ -150,13 +189,243 @@ pub async fn handler( None => filter, }; - let page = repo.user().list(filter, pagination).await?; - let count = repo.user().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo.user().list(filter, pagination).await?; + let count = repo.user().count(filter).await?; + PaginatedResponse::for_page(page.map(User::from), pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo.user().list(filter, pagination).await?; + PaginatedResponse::for_page(page.map(User::from), pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.user().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(User::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_list_users(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + let mut rng = state.rng(); + + // Provision two users + let mut repo = state.repository().await.unwrap(); + repo.user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + repo.user() + .add(&mut rng, &state.clock, "bob".to_owned()) + .await + .unwrap(); + repo.save().await.unwrap(); + + // Test default behavior (count=true) + let request = Request::get("/api/admin/v1/users").bearer(&token).empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 2 + }, + "data": [ + { + "type": "user", + "id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "attributes": { + "username": "bob", + "created_at": "2022-01-16T14:40:00Z", + "locked_at": null, + "deactivated_at": null, + "admin": false, + "legacy_guest": false + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + } + }, + { + "type": "user", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "username": "alice", + "created_at": "2022-01-16T14:40:00Z", + "locked_at": null, + "deactivated_at": null, + "admin": false, + "legacy_guest": false + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/users?page[first]=10", + "first": "/api/admin/v1/users?page[first]=10", + "last": "/api/admin/v1/users?page[last]=10" + } + } + "#); + + // Test count=false + let request = Request::get("/api/admin/v1/users?count=false") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user", + "id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "attributes": { + "username": "bob", + "created_at": "2022-01-16T14:40:00Z", + "locked_at": null, + "deactivated_at": null, + "admin": false, + "legacy_guest": false + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + } + }, + { + "type": "user", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "username": "alice", + "created_at": "2022-01-16T14:40:00Z", + "locked_at": null, + "deactivated_at": null, + "admin": false, + "legacy_guest": false + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/users?count=false&page[first]=10", + "first": "/api/admin/v1/users?count=false&page[first]=10", + "last": "/api/admin/v1/users?count=false&page[last]=10" + } + } + "#); + + // Test count=only + let request = Request::get("/api/admin/v1/users?count=only") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "meta": { + "count": 2 + }, + "links": { + "self": "/api/admin/v1/users?count=only" + } + } + "###); + + // Test count=false with filtering + let request = Request::get("/api/admin/v1/users?count=false&filter[search]=alice") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "data": [ + { + "type": "user", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "username": "alice", + "created_at": "2022-01-16T14:40:00Z", + "locked_at": null, + "deactivated_at": null, + "admin": false, + "legacy_guest": false + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + } + ], + "links": { + "self": "/api/admin/v1/users?filter[search]=alice&count=false&page[first]=10", + "first": "/api/admin/v1/users?filter[search]=alice&count=false&page[first]=10", + "last": "/api/admin/v1/users?filter[search]=alice&count=false&page[last]=10" + } + } + "#); + + // Test count=only with filtering + let request = Request::get("/api/admin/v1/users?count=only&filter[search]=alice") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "links": { + "self": "/api/admin/v1/users?filter[search]=alice&count=only" + } + } + "#); + } } diff --git a/crates/handlers/src/admin/v1/users/set_password.rs b/crates/handlers/src/admin/v1/users/set_password.rs index 390c31e47..7e9365f73 100644 --- a/crates/handlers/src/admin/v1/users/set_password.rs +++ b/crates/handlers/src/admin/v1/users/set_password.rs @@ -55,16 +55,12 @@ impl IntoResponse for RouteError { } } -fn password_example() -> String { - "hunter2".to_owned() -} - /// # JSON payload for the `POST /api/admin/v1/users/:id/set-password` endpoint #[derive(Deserialize, JsonSchema)] #[schemars(rename = "SetUserPasswordRequest")] pub struct Request { /// The password to set for the user - #[schemars(example = "password_example")] + #[schemars(example = &"hunter2")] password: String, /// Skip the password complexity check diff --git a/crates/handlers/src/admin/v1/version.rs b/crates/handlers/src/admin/v1/version.rs new file mode 100644 index 000000000..2fe53940b --- /dev/null +++ b/crates/handlers/src/admin/v1/version.rs @@ -0,0 +1,62 @@ +// 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. + +use aide::transform::TransformOperation; +use axum::{Json, extract::State}; +use mas_data_model::AppVersion; +use schemars::JsonSchema; +use serde::Serialize; + +use crate::admin::call_context::CallContext; + +#[derive(Serialize, JsonSchema)] +pub struct Version { + /// The semver version of the app + pub version: &'static str, +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("version") + .tag("server") + .summary("Get the version currently running") + .response_with::<200, Json, _>(|t| t.example(Version { version: "v1.0.0" })) +} + +#[tracing::instrument(name = "handler.admin.v1.version", skip_all)] +pub async fn handler( + _: CallContext, + State(AppVersion(version)): State, +) -> Json { + Json(Version { version }) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use insta::assert_json_snapshot; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_add_user(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request = Request::get("/api/admin/v1/version").bearer(&token).empty(); + + let response = state.request(request).await; + + assert_eq!(response.status(), StatusCode::OK); + let body: serde_json::Value = response.json(); + assert_json_snapshot!(body, @r#" + { + "version": "v0.0.0-test" + } + "#); + } +} diff --git a/crates/handlers/src/bin/api-schema.rs b/crates/handlers/src/bin/api-schema.rs index 894546961..1b73c05c3 100644 --- a/crates/handlers/src/bin/api-schema.rs +++ b/crates/handlers/src/bin/api-schema.rs @@ -59,6 +59,8 @@ impl_from_ref!(Arc); impl_from_ref!(mas_keystore::Keystore); impl_from_ref!(mas_handlers::passwords::PasswordManager); impl_from_ref!(Arc); +impl_from_ref!(mas_data_model::SiteConfig); +impl_from_ref!(mas_data_model::AppVersion); fn main() -> Result<(), Box> { let (mut api, _) = mas_handlers::admin_api_router::(); diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index 93b9f9dee..a4fbb24fb 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -8,9 +8,10 @@ use std::collections::HashMap; use anyhow::Context; use axum::{ - extract::{Form, Path, Query, State}, + extract::{Form, Path, State}, response::{Html, IntoResponse, Redirect, Response}, }; +use axum_extra::extract::Query; use chrono::Duration; use mas_axum_utils::{ InternalError, diff --git a/crates/handlers/src/compat/login_sso_redirect.rs b/crates/handlers/src/compat/login_sso_redirect.rs index 09af59b45..f085bb82f 100644 --- a/crates/handlers/src/compat/login_sso_redirect.rs +++ b/crates/handlers/src/compat/login_sso_redirect.rs @@ -4,10 +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 axum::{ - extract::{Query, State}, - response::IntoResponse, -}; +use axum::{extract::State, response::IntoResponse}; +use axum_extra::extract::Query; use hyper::StatusCode; use mas_axum_utils::{GenericError, InternalError}; use mas_data_model::{BoxClock, BoxRng}; diff --git a/crates/handlers/src/graphql/model/browser_sessions.rs b/crates/handlers/src/graphql/model/browser_sessions.rs index 925288067..08ba25830 100644 --- a/crates/handlers/src/graphql/model/browser_sessions.rs +++ b/crates/handlers/src/graphql/model/browser_sessions.rs @@ -172,7 +172,7 @@ impl BrowserSession { connection .edges - .extend(page.edges.into_iter().map(|s| match s { + .extend(page.edges.into_iter().map(|edge| match edge.node { mas_storage::app_session::AppSession::Compat(session) => Edge::new( OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), AppSession::CompatSession(Box::new(CompatSession::new(*session))), diff --git a/crates/handlers/src/graphql/model/users.rs b/crates/handlers/src/graphql/model/users.rs index 11522c6b4..7e615df7d 100644 --- a/crates/handlers/src/graphql/model/users.rs +++ b/crates/handlers/src/graphql/model/users.rs @@ -125,10 +125,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|u| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, u.id)), - CompatSsoLogin(u), + OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, edge.cursor)), + CompatSsoLogin(edge.node), ) })); @@ -219,14 +219,13 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection - .edges - .extend(page.edges.into_iter().map(|(session, sso_login)| { - Edge::new( - OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), - CompatSession::new(session).with_loaded_sso_login(sso_login), - ) - })); + connection.edges.extend(page.edges.into_iter().map(|edge| { + let (session, sso_login) = edge.node; + Edge::new( + OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), + CompatSession::new(session).with_loaded_sso_login(sso_login), + ) + })); Ok::<_, async_graphql::Error>(connection) }, @@ -305,10 +304,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|u| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)), - BrowserSession(u), + OpaqueCursor(NodeCursor(NodeType::BrowserSession, edge.cursor)), + BrowserSession(edge.node), ) })); @@ -373,10 +372,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|u| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::UserEmail, u.id)), - UserEmail(u), + OpaqueCursor(NodeCursor(NodeType::UserEmail, edge.cursor)), + UserEmail(edge.node), ) })); @@ -480,10 +479,10 @@ impl User { PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|s| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::OAuth2Session, s.id)), - OAuth2Session(s), + OpaqueCursor(NodeCursor(NodeType::OAuth2Session, edge.cursor)), + OAuth2Session(edge.node), ) })); @@ -547,10 +546,10 @@ impl User { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|s| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Link, s.id)), - UpstreamOAuth2Link::new(s), + OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Link, edge.cursor)), + UpstreamOAuth2Link::new(edge.node), ) })); @@ -689,13 +688,13 @@ impl User { connection .edges - .extend(page.edges.into_iter().map(|s| match s { + .extend(page.edges.into_iter().map(|edge| match edge.node { mas_storage::app_session::AppSession::Compat(session) => Edge::new( - OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)), + OpaqueCursor(NodeCursor(NodeType::CompatSession, edge.cursor)), AppSession::CompatSession(Box::new(CompatSession::new(*session))), ), mas_storage::app_session::AppSession::OAuth2(session) => Edge::new( - OpaqueCursor(NodeCursor(NodeType::OAuth2Session, session.id)), + OpaqueCursor(NodeCursor(NodeType::OAuth2Session, edge.cursor)), AppSession::OAuth2Session(Box::new(OAuth2Session(*session))), ), })); diff --git a/crates/handlers/src/graphql/mutations/mod.rs b/crates/handlers/src/graphql/mutations/mod.rs index af6caab62..a84bf9210 100644 --- a/crates/handlers/src/graphql/mutations/mod.rs +++ b/crates/handlers/src/graphql/mutations/mod.rs @@ -84,7 +84,7 @@ async fn verify_password_if_needed( password, user_password.hashed_password, ) - .await; + .await?; - Ok(res.is_ok()) + Ok(res.is_success()) } diff --git a/crates/handlers/src/graphql/mutations/user.rs b/crates/handlers/src/graphql/mutations/user.rs index f9f5696e7..355c7d0ac 100644 --- a/crates/handlers/src/graphql/mutations/user.rs +++ b/crates/handlers/src/graphql/mutations/user.rs @@ -737,13 +737,14 @@ impl UserMutations { )); }; - if let Err(_err) = password_manager + if !password_manager .verify( active_password.version, Zeroizing::new(current_password_attempt), active_password.hashed_password, ) - .await + .await? + .is_success() { return Ok(SetPasswordPayload { status: SetPasswordStatus::WrongPassword, diff --git a/crates/handlers/src/graphql/query/session.rs b/crates/handlers/src/graphql/query/session.rs index 921009ee9..82ca55fd9 100644 --- a/crates/handlers/src/graphql/query/session.rs +++ b/crates/handlers/src/graphql/query/session.rs @@ -68,7 +68,8 @@ impl SessionQuery { ); } - if let Some((compat_session, sso_login)) = compat_sessions.edges.into_iter().next() { + if let Some(edge) = compat_sessions.edges.into_iter().next() { + let (compat_session, sso_login) = edge.node; repo.cancel().await?; return Ok(Some(Session::CompatSession(Box::new( @@ -92,10 +93,10 @@ impl SessionQuery { ); } - if let Some(session) = sessions.edges.into_iter().next() { + if let Some(edge) = sessions.edges.into_iter().next() { repo.cancel().await?; return Ok(Some(Session::OAuth2Session(Box::new(OAuth2Session( - session, + edge.node, ))))); } repo.cancel().await?; diff --git a/crates/handlers/src/graphql/query/upstream_oauth.rs b/crates/handlers/src/graphql/query/upstream_oauth.rs index f52c21f82..f0b4ceee6 100644 --- a/crates/handlers/src/graphql/query/upstream_oauth.rs +++ b/crates/handlers/src/graphql/query/upstream_oauth.rs @@ -130,10 +130,10 @@ impl UpstreamOAuthQuery { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend(page.edges.into_iter().map(|p| { + connection.edges.extend(page.edges.into_iter().map(|edge| { Edge::new( - OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Provider, p.id)), - UpstreamOAuth2Provider::new(p), + OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Provider, edge.cursor)), + UpstreamOAuth2Provider::new(edge.node), ) })); diff --git a/crates/handlers/src/graphql/query/user.rs b/crates/handlers/src/graphql/query/user.rs index 364319e57..bb55ef67b 100644 --- a/crates/handlers/src/graphql/query/user.rs +++ b/crates/handlers/src/graphql/query/user.rs @@ -143,11 +143,12 @@ impl UserQuery { page.has_next_page, PreloadedTotalCount(count), ); - connection.edges.extend( - page.edges.into_iter().map(|p| { - Edge::new(OpaqueCursor(NodeCursor(NodeType::User, p.id)), User(p)) - }), - ); + connection.edges.extend(page.edges.into_iter().map(|edge| { + Edge::new( + OpaqueCursor(NodeCursor(NodeType::User, edge.cursor)), + User(edge.node), + ) + })); Ok::<_, async_graphql::Error>(connection) }, diff --git a/crates/handlers/src/graphql/tests.rs b/crates/handlers/src/graphql/tests.rs index 328b6f152..888d477d0 100644 --- a/crates/handlers/src/graphql/tests.rs +++ b/crates/handlers/src/graphql/tests.rs @@ -6,6 +6,7 @@ use axum::http::Request; use hyper::StatusCode; +use mas_axum_utils::SessionInfoExt; use mas_data_model::{AccessToken, Client, TokenType, User}; use mas_matrix::{HomeserverConnection, ProvisionRequest}; use mas_router::SimpleRoute; @@ -19,11 +20,9 @@ use oauth2_types::{ scope::{OPENID, Scope, ScopeToken}, }; use sqlx::PgPool; +use zeroize::Zeroizing; -use crate::{ - test_utils, - test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}, -}; +use crate::test_utils::{self, CookieHelper, RequestBuilderExt, ResponseExt, TestState, setup}; async fn create_test_client(state: &TestState) -> Client { let mut repo = state.repository().await.unwrap(); @@ -781,3 +780,301 @@ async fn test_add_user(pool: PgPool) { }) ); } + +/// Test the setPassword mutation where the current password provided is +/// wrong. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_set_password_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let user_id = user.id; + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": format!(r#" + mutation {{ + setPassword(input: {{ + userId: "user:{user_id}", + currentPassword: "wrong.password.123", + newPassword: "new.password.123" + }}) {{ + status + }} + }} + "#), + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["setPassword"]["status"].as_str(), + Some("WRONG_PASSWORD"), + "{:?}", + response.data + ); +} + +/// Test the startEmailAuthentication mutation where the current password +/// provided is invalid. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_start_email_authentication_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": r#" + mutation { + startEmailAuthentication(input: { + email: "alice@example.org", + password: "wrong.password.123" + }) { + status + } + } + "#, + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["startEmailAuthentication"]["status"].as_str(), + Some("INCORRECT_PASSWORD"), + "{:?}", + response.data + ); +} + +/// Test the removeEmail mutation where the current password +/// provided is invalid. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_remove_email_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let user_email_id = repo + .user_email() + .add( + &mut rng, + &state.clock, + &user, + "alice@example.org".to_owned(), + ) + .await + .unwrap() + .id; + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": format!(r#" + mutation {{ + removeEmail(input: {{ + userEmailId: "user_email:{user_email_id}", + password: "wrong.password.123" + }}) {{ + status + }} + }} + "#), + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["removeEmail"]["status"].as_str(), + Some("INCORRECT_PASSWORD"), + "{:?}", + response.data + ); +} + +/// Test the deactivateUser mutation where the current password +/// provided is invalid. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_deactivate_user_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": r#" + mutation { + deactivateUser(input: { + hsErase: true, + password: "wrong.password.123" + }) { + status + } + } + "#, + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["deactivateUser"]["status"].as_str(), + Some("INCORRECT_PASSWORD"), + "{:?}", + response.data + ); +} diff --git a/crates/handlers/src/oauth2/device/link.rs b/crates/handlers/src/oauth2/device/link.rs index da2fb2700..84d0c5077 100644 --- a/crates/handlers/src/oauth2/device/link.rs +++ b/crates/handlers/src/oauth2/device/link.rs @@ -5,9 +5,10 @@ // Please see LICENSE files in the repository root for full details. use axum::{ - extract::{Query, State}, + extract::State, response::{Html, IntoResponse}, }; +use axum_extra::extract::Query; use mas_axum_utils::{InternalError, cookies::CookieJar}; use mas_data_model::BoxClock; use mas_router::UrlBuilder; diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 6bb61bf72..17f508921 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -15,7 +15,9 @@ use mas_axum_utils::{ client_authorization::{ClientAuthorization, CredentialsVerificationError}, record_error, }; -use mas_data_model::{BoxClock, Clock, Device, TokenFormatError, TokenType}; +use mas_data_model::{ + BoxClock, Clock, Device, TokenFormatError, TokenType, personal::session::PersonalSessionOwner, +}; use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; use mas_keystore::Encrypter; use mas_matrix::HomeserverConnection; @@ -93,6 +95,14 @@ pub enum RouteError { #[error("unknown compat session {0}")] CantLoadCompatSession(Ulid), + /// The personal access token session is not valid. + #[error("invalid personal access token session {0}")] + InvalidPersonalSession(Ulid), + + /// The personal access token session could not be found in the database. + #[error("unknown personal access token session {0}")] + CantLoadPersonalSession(Ulid), + /// The Device ID in the compat session can't be encoded as a scope #[error("device ID contains characters that are not allowed in a scope")] CantEncodeDeviceID(#[from] mas_data_model::ToScopeTokenError), @@ -103,6 +113,9 @@ pub enum RouteError { #[error("unknown user {0}")] CantLoadUser(Ulid), + #[error("unknown OAuth2 client {0}")] + CantLoadOAuth2Client(Ulid), + #[error("bad request")] BadRequest, @@ -131,7 +144,9 @@ impl IntoResponse for RouteError { e @ (Self::Internal(_) | Self::CantLoadCompatSession(_) | Self::CantLoadOAuthSession(_) + | Self::CantLoadPersonalSession(_) | Self::CantLoadUser(_) + | Self::CantLoadOAuth2Client(_) | Self::FailedToVerifyToken(_)) => ( StatusCode::INTERNAL_SERVER_ERROR, Json( @@ -167,6 +182,7 @@ impl IntoResponse for RouteError { | Self::InvalidUser(_) | Self::InvalidCompatSession(_) | Self::InvalidOAuthSession(_) + | Self::InvalidPersonalSession(_) | Self::InvalidTokenFormat(_) | Self::CantEncodeDeviceID(_) => { INTROSPECTION_COUNTER.add(1, &[KeyValue::new(ACTIVE.clone(), false)]); @@ -625,6 +641,97 @@ pub(crate) async fn post( device_id: session.device.map(Device::into), } } + + TokenType::PersonalAccessToken => { + let access_token = repo + .personal_access_token() + .find_by_token(token) + .await? + .ok_or(RouteError::UnknownToken(TokenType::AccessToken))?; + + if !access_token.is_valid(clock.now()) { + return Err(RouteError::InvalidToken(TokenType::AccessToken)); + } + + let session = repo + .personal_session() + .lookup(access_token.session_id) + .await? + .ok_or(RouteError::CantLoadPersonalSession(access_token.session_id))?; + + if !session.is_valid() { + return Err(RouteError::InvalidPersonalSession(session.id)); + } + + let actor_user = repo + .user() + .lookup(session.actor_user_id) + .await? + .ok_or(RouteError::CantLoadUser(session.actor_user_id))?; + + if !actor_user.is_valid() { + return Err(RouteError::InvalidUser(actor_user.id)); + } + + let client_id = match session.owner { + PersonalSessionOwner::User(owner_user_id) => { + let owner_user = repo + .user() + .lookup(owner_user_id) + .await? + .ok_or(RouteError::CantLoadUser(owner_user_id))?; + + if !owner_user.is_valid() { + return Err(RouteError::InvalidUser(owner_user.id)); + } + + None + } + PersonalSessionOwner::OAuth2Client(owner_client_id) => { + let owner_client = repo + .oauth2_client() + .lookup(owner_client_id) + .await? + .ok_or(RouteError::CantLoadOAuth2Client(owner_client_id))?; + + // OAuth2 clients are always valid if they're in the database + Some(owner_client.client_id.clone()) + } + }; + + activity_tracker + .record_personal_session(&clock, &session, ip) + .await; + + INTROSPECTION_COUNTER.add( + 1, + &[ + KeyValue::new(KIND, "personal_access_token"), + KeyValue::new(ACTIVE, true), + ], + ); + + let scope = normalize_scope(session.scope); + + IntrospectionResponse { + active: true, + scope: Some(scope), + client_id, + username: Some(actor_user.username), + token_type: Some(OAuthTokenTypeHint::AccessToken), + exp: access_token.expires_at, + expires_in: access_token + .expires_at + .map(|expires_at| expires_at.signed_duration_since(clock.now())), + iat: Some(access_token.created_at), + nbf: Some(access_token.created_at), + sub: Some(actor_user.sub), + aud: None, + iss: None, + jti: None, + device_id: None, + } + } }; repo.save().await?; @@ -636,7 +743,9 @@ pub(crate) async fn post( mod tests { use chrono::Duration; use hyper::{Request, StatusCode}; - use mas_data_model::{AccessToken, Clock, RefreshToken}; + use mas_data_model::{ + AccessToken, Clock, RefreshToken, TokenType, personal::session::PersonalSessionOwner, + }; use mas_iana::oauth::OAuthTokenTypeHint; use mas_matrix::{HomeserverConnection, MockHomeserverConnection, ProvisionRequest}; use mas_router::{OAuth2Introspection, OAuth2RegistrationEndpoint, SimpleRoute}; @@ -1069,4 +1178,125 @@ mod tests { let response: ClientError = response.json(); assert_eq!(response.error, ClientErrorCode::AccessDenied); } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_introspect_personal_access_tokens(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + // Provision a client which will be used to do introspection requests + let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({ + "client_uri": "https://introspecting.com/", + "grant_types": [], + "token_endpoint_auth_method": "client_secret_basic", + })); + + let response = state.request(request).await; + response.assert_status(StatusCode::CREATED); + let client: ClientRegistrationResponse = response.json(); + let introspecting_client_id = client.client_id; + let introspecting_client_secret = client.client_secret.unwrap(); + + let mut repo = state.repository().await.unwrap(); + + // Provision an owner user (who provisions the personal session) + let owner_user = repo + .user() + .add(&mut state.rng(), &state.clock, "admin".to_owned()) + .await + .unwrap(); + + // Provision an actor user (which the token represents) + let actor_user = repo + .user() + .add(&mut state.rng(), &state.clock, "bruce".to_owned()) + .await + .unwrap(); + + // admin creates a personal session to control bruce's account + let personal_session = repo + .personal_session() + .add( + &mut state.rng(), + &state.clock, + PersonalSessionOwner::User(owner_user.id), + &actor_user, + "Test Personal Access Token".to_owned(), + Scope::from_iter([OPENID]), + ) + .await + .unwrap(); + + // Generate a personal access token with proper token format + let token_string = TokenType::PersonalAccessToken.generate(&mut state.rng()); + let _personal_access_token = repo + .personal_access_token() + .add( + &mut state.rng(), + &state.clock, + &personal_session, + &token_string, + Some(Duration::try_hours(1).unwrap()), + ) + .await + .unwrap(); + + repo.save().await.unwrap(); + + // Now that we have a personal access token, we can introspect it + let request = Request::post(OAuth2Introspection::PATH) + .basic_auth(&introspecting_client_id, &introspecting_client_secret) + .form(json!({ "token": token_string })); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: IntrospectionResponse = response.json(); + assert!(response.active); + // Actor user + assert_eq!(response.username, Some("bruce".to_owned())); + // Not owned by a client + assert_eq!(response.client_id, None); + assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken)); + assert_eq!(response.scope, Some(Scope::from_iter([OPENID]))); + + // Do the same request, but with a token_type_hint + let last_active = state.clock.now(); + let request = Request::post(OAuth2Introspection::PATH) + .basic_auth(&introspecting_client_id, &introspecting_client_secret) + .form(json!({"token": token_string, "token_type_hint": "access_token"})); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: IntrospectionResponse = response.json(); + assert!(response.active); + + // Do the same request, but with the wrong token_type_hint + let request = Request::post(OAuth2Introspection::PATH) + .basic_auth(&introspecting_client_id, &introspecting_client_secret) + .form(json!({"token": token_string, "token_type_hint": "refresh_token"})); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: IntrospectionResponse = response.json(); + assert!(!response.active); // It shouldn't be active with wrong hint + + // Advance the clock to invalidate the access token + state.clock.advance(Duration::try_hours(2).unwrap()); + + let request = Request::post(OAuth2Introspection::PATH) + .basic_auth(&introspecting_client_id, &introspecting_client_secret) + .form(json!({ "token": token_string })); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: IntrospectionResponse = response.json(); + assert!(!response.active); // It shouldn't be active anymore + + state.activity_tracker.flush().await; + let mut repo = state.repository().await.unwrap(); + let session = repo + .personal_session() + .lookup(personal_session.id) + .await + .unwrap() + .unwrap(); + assert_eq!(session.last_active_at, Some(last_active)); + repo.save().await.unwrap(); + } } diff --git a/crates/handlers/src/oauth2/webfinger.rs b/crates/handlers/src/oauth2/webfinger.rs index 8289e495c..489a8e9ef 100644 --- a/crates/handlers/src/oauth2/webfinger.rs +++ b/crates/handlers/src/oauth2/webfinger.rs @@ -4,12 +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 axum::{ - Json, - extract::{Query, State}, - response::IntoResponse, -}; -use axum_extra::typed_header::TypedHeader; +use axum::{Json, extract::State, response::IntoResponse}; +use axum_extra::{extract::Query, typed_header::TypedHeader}; use headers::ContentType; use mas_router::UrlBuilder; use oauth2_types::webfinger::WebFingerResponse; diff --git a/crates/handlers/src/passwords.rs b/crates/handlers/src/passwords.rs index 6f32f77f9..6071cf730 100644 --- a/crates/handlers/src/passwords.rs +++ b/crates/handlers/src/passwords.rs @@ -49,6 +49,11 @@ impl PasswordVerificationResult { Self::Failure => PasswordVerificationResult::Failure, } } + + #[must_use] + pub fn is_success(&self) -> bool { + matches!(self, Self::Success(_)) + } } impl From for PasswordVerificationResult<()> { diff --git a/crates/handlers/src/rate_limit.rs b/crates/handlers/src/rate_limit.rs index 8b2485941..0471e6351 100644 --- a/crates/handlers/src/rate_limit.rs +++ b/crates/handlers/src/rate_limit.rs @@ -328,6 +328,7 @@ mod tests { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: true, }; let bob = User { @@ -338,6 +339,7 @@ mod tests { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: true, }; // Three times the same IP address should be allowed diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index e43194776..f1859f352 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -28,7 +28,7 @@ use mas_axum_utils::{ cookies::{CookieJar, CookieManager}, }; use mas_config::RateLimitingConfig; -use mas_data_model::{BoxClock, BoxRng, SiteConfig, clock::MockClock}; +use mas_data_model::{AppVersion, BoxClock, BoxRng, SiteConfig, clock::MockClock}; use mas_email::{MailTransport, Mailer}; use mas_i18n::Translator; use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey}; @@ -140,6 +140,7 @@ pub fn test_site_config() -> SiteConfig { email_change_allowed: true, displayname_change_allowed: true, password_change_allowed: true, + password_registration_email_required: true, account_recovery_allowed: true, account_deactivation_allowed: true, captcha: None, @@ -175,6 +176,8 @@ impl TestState { workspace_root.join("translations"), site_config.templates_branding(), site_config.templates_features(), + // Strict mode in testing + true, ) .await?; @@ -575,6 +578,12 @@ impl FromRef for reqwest::Client { } } +impl FromRef for AppVersion { + fn from_ref(_input: &TestState) -> Self { + AppVersion("v0.0.0-test") + } +} + impl FromRequestParts for ActivityTracker { type Rejection = Infallible; diff --git a/crates/handlers/src/upstream_oauth2/authorize.rs b/crates/handlers/src/upstream_oauth2/authorize.rs index 8d66c2ba5..8749f3c3d 100644 --- a/crates/handlers/src/upstream_oauth2/authorize.rs +++ b/crates/handlers/src/upstream_oauth2/authorize.rs @@ -5,9 +5,10 @@ // Please see LICENSE files in the repository root for full details. use axum::{ - extract::{Path, Query, State}, + extract::{Path, State}, response::{IntoResponse, Redirect}, }; +use axum_extra::extract::Query; use hyper::StatusCode; use mas_axum_utils::{GenericError, InternalError, cookies::CookieJar}; use mas_data_model::{BoxClock, BoxRng, UpstreamOAuthProvider}; diff --git a/crates/handlers/src/upstream_oauth2/backchannel_logout.rs b/crates/handlers/src/upstream_oauth2/backchannel_logout.rs index 76a7b574c..63454741c 100644 --- a/crates/handlers/src/upstream_oauth2/backchannel_logout.rs +++ b/crates/handlers/src/upstream_oauth2/backchannel_logout.rs @@ -267,9 +267,9 @@ pub(crate) async fn post( .browser_session() .list(browser_session_filter, cursor) .await?; - for browser_session in browser_sessions.edges { - user_ids.insert(browser_session.user.id); - cursor = cursor.after(browser_session.id); + for edge in browser_sessions.edges { + user_ids.insert(edge.node.user.id); + cursor = cursor.after(edge.cursor); } if !browser_sessions.has_next_page { diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index a3d4c1bb9..d9577bafd 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -1212,9 +1212,9 @@ mod tests { .list(UserEmailFilter::new().for_user(&user), Pagination::first(1)) .await .unwrap(); - let email = page.edges.first().expect("email exists"); + let edge = page.edges.first().expect("email exists"); - assert_eq!(email.email, "john@example.com"); + assert_eq!(edge.node.email, "john@example.com"); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/views/app.rs b/crates/handlers/src/views/app.rs index 38c93bac4..4ae5f5222 100644 --- a/crates/handlers/src/views/app.rs +++ b/crates/handlers/src/views/app.rs @@ -5,9 +5,10 @@ // Please see LICENSE files in the repository root for full details. use axum::{ - extract::{Query, State}, + extract::State, response::{Html, IntoResponse}, }; +use axum_extra::extract::Query; use mas_axum_utils::{InternalError, cookies::CookieJar}; use mas_data_model::{BoxClock, BoxRng}; use mas_router::{PostAuthAction, UrlBuilder}; diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 57091e5fc..72e1566fe 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -7,10 +7,10 @@ use std::sync::{Arc, LazyLock}; use axum::{ - extract::{Form, Query, State}, + extract::{Form, State}, response::{Html, IntoResponse, Response}, }; -use axum_extra::typed_header::TypedHeader; +use axum_extra::{extract::Query, typed_header::TypedHeader}; use hyper::StatusCode; use mas_axum_utils::{ InternalError, SessionInfoExt, diff --git a/crates/handlers/src/views/register/mod.rs b/crates/handlers/src/views/register/mod.rs index 41ee18ad6..ad7867a39 100644 --- a/crates/handlers/src/views/register/mod.rs +++ b/crates/handlers/src/views/register/mod.rs @@ -4,9 +4,10 @@ // Please see LICENSE files in the repository root for full details. use axum::{ - extract::{Query, State}, + extract::State, response::{Html, IntoResponse, Response}, }; +use axum_extra::extract::Query; use mas_axum_utils::{InternalError, SessionInfoExt, cookies::CookieJar, csrf::CsrfExt as _}; use mas_data_model::{BoxClock, BoxRng, SiteConfig}; use mas_router::{PasswordRegister, UpstreamOAuth2Authorize, UrlBuilder}; diff --git a/crates/handlers/src/views/register/password.rs b/crates/handlers/src/views/register/password.rs index a9a882f68..65ba5fe0d 100644 --- a/crates/handlers/src/views/register/password.rs +++ b/crates/handlers/src/views/register/password.rs @@ -7,10 +7,10 @@ use std::{str::FromStr, sync::Arc}; use axum::{ - extract::{Form, Query, State}, + extract::{Form, State}, response::{Html, IntoResponse, Response}, }; -use axum_extra::typed_header::TypedHeader; +use axum_extra::{extract::Query, typed_header::TypedHeader}; use hyper::StatusCode; use lettre::Address; use mas_axum_utils::{ @@ -45,6 +45,7 @@ use crate::{ #[derive(Debug, Deserialize, Serialize)] pub(crate) struct RegisterForm { username: String, + #[serde(default)] email: String, password: String, password_confirm: String, @@ -165,9 +166,16 @@ pub(crate) async fn post( .await .is_ok(); + let state = form.to_form_state(); + + // The email form is only shown if the server requires it + let email = site_config + .password_registration_email_required + .then_some(form.email); + // Validate the form let state = { - let mut state = form.to_form_state(); + let mut state = state; if !passed_captcha { state.add_error_on_form(FormError::Captcha); @@ -195,13 +203,15 @@ pub(crate) async fn post( homeserver_denied_username = true; } - // Note that we don't check here if the email is already taken here, as - // we don't want to leak the information about other users. Instead, we will - // show an error message once the user confirmed their email address. - if form.email.is_empty() { - state.add_error_on_field(RegisterFormField::Email, FieldError::Required); - } else if Address::from_str(&form.email).is_err() { - state.add_error_on_field(RegisterFormField::Email, FieldError::Invalid); + if let Some(email) = &email { + // Note that we don't check here if the email is already taken here, as + // we don't want to leak the information about other users. Instead, we will + // show an error message once the user confirmed their email address. + if email.is_empty() { + state.add_error_on_field(RegisterFormField::Email, FieldError::Required); + } else if Address::from_str(email).is_err() { + state.add_error_on_field(RegisterFormField::Email, FieldError::Invalid); + } } if form.password.is_empty() { @@ -240,7 +250,7 @@ pub(crate) async fn post( .evaluate_register(mas_policy::RegisterInput { registration_method: mas_policy::RegistrationMethod::Password, username: &form.username, - email: Some(&form.email), + email: email.as_deref(), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), user_agent: user_agent.clone(), @@ -295,7 +305,9 @@ pub(crate) async fn post( state.add_error_on_form(FormError::RateLimitExceeded); } - if let Err(e) = limiter.check_email_authentication_email(requester, &form.email) { + if let Some(email) = &email + && let Err(e) = limiter.check_email_authentication_email(requester, email) + { tracing::warn!(error = &e as &dyn std::error::Error); state.add_error_on_form(FormError::RateLimitExceeded); } @@ -343,25 +355,28 @@ pub(crate) async fn post( registration }; - // Create a new user email authentication session - let user_email_authentication = repo - .user_email() - .add_authentication_for_registration(&mut rng, &clock, form.email, ®istration) - .await?; + let registration = if let Some(email) = email { + // Create a new user email authentication session + let user_email_authentication = repo + .user_email() + .add_authentication_for_registration(&mut rng, &clock, email, ®istration) + .await?; - // Schedule a job to verify the email - repo.queue_job() - .schedule_job( - &mut rng, - &clock, - SendEmailAuthenticationCodeJob::new(&user_email_authentication, locale.to_string()), - ) - .await?; + // Schedule a job to verify the email + repo.queue_job() + .schedule_job( + &mut rng, + &clock, + SendEmailAuthenticationCodeJob::new(&user_email_authentication, locale.to_string()), + ) + .await?; - let registration = repo - .user_registration() - .set_email_authentication(registration, &user_email_authentication) - .await?; + repo.user_registration() + .set_email_authentication(registration, &user_email_authentication) + .await? + } else { + registration + }; // Hash the password let password = Zeroizing::new(form.password); @@ -713,4 +728,319 @@ mod tests { response.assert_status(StatusCode::OK); assert!(response.body().contains("This username is already taken")); } + + /// Test registration without email when email is not required + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_without_email_when_not_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: false, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form without email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "alice", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::SEE_OTHER); + let location = response.headers().get(LOCATION).unwrap(); + + // The handler redirects with the ID as the second to last portion of the path + let id = location + .to_str() + .unwrap() + .rsplit('/') + .nth(1) + .unwrap() + .parse() + .unwrap(); + + // There should be a new registration in the database + let mut repo = state.repository().await.unwrap(); + let registration = repo.user_registration().lookup(id).await.unwrap().unwrap(); + assert_eq!(registration.username, "alice".to_owned()); + assert!(registration.password.is_some()); + // Email authentication should be None when email is not required and not + // provided + assert!(registration.email_authentication_id.is_none()); + } + + /// Test registration with valid email when email is not required + /// (email input is ignored completely when not required) + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_with_email_when_not_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: false, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form with valid email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "charlie", + "email": "charlie@example.com", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::SEE_OTHER); + let location = response.headers().get(LOCATION).unwrap(); + + // The handler redirects with the ID as the second to last portion of the path + let id = location + .to_str() + .unwrap() + .rsplit('/') + .nth(1) + .unwrap() + .parse() + .unwrap(); + + // There should be a new registration in the database + let mut repo = state.repository().await.unwrap(); + let registration = repo.user_registration().lookup(id).await.unwrap().unwrap(); + assert_eq!(registration.username, "charlie".to_owned()); + assert!(registration.password.is_some()); + + // Email authentication should be None when email is not required + // (email input is completely ignored in this case) + assert!(registration.email_authentication_id.is_none()); + } + + /// Test registration fails when email is required but not provided + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_fails_without_email_when_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: true, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form without email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "david", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Check that the response contains an error about the email field + let body = response.body(); + assert!(body.contains("email") || body.contains("Email")); + + // Ensure no registration was created + let mut repo = state.repository().await.unwrap(); + let user_exists = repo.user().exists("david").await.unwrap(); + assert!(!user_exists); + } + + /// Test registration fails when email is required but empty + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_fails_with_empty_email_when_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: true, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form with empty email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "eve", + "email": "", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Check that the response contains an error about the email field + let body = response.body(); + assert!(body.contains("email") || body.contains("Email")); + + // Ensure no registration was created + let mut repo = state.repository().await.unwrap(); + let user_exists = repo.user().exists("eve").await.unwrap(); + assert!(!user_exists); + } + + /// Test registration fails with invalid email when email is required + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_fails_with_invalid_email_when_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: true, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form with invalid email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "grace", + "email": "not-an-email", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Check that the response contains an error about the email field + let body = response.body(); + assert!(body.contains("email") || body.contains("Email")); + + // Ensure no registration was created + let mut repo = state.repository().await.unwrap(); + let user_exists = repo.user().exists("grace").await.unwrap(); + assert!(!user_exists); + } } diff --git a/crates/handlers/src/views/register/steps/finish.rs b/crates/handlers/src/views/register/steps/finish.rs index de7a537b5..e1ed8a3f0 100644 --- a/crates/handlers/src/views/register/steps/finish.rs +++ b/crates/handlers/src/views/register/steps/finish.rs @@ -151,52 +151,62 @@ pub(crate) async fn get( None }; - // For now, we require an email address on the registration, but this might - // change in the future - let email_authentication_id = registration - .email_authentication_id - .context("No email authentication started for this registration") - .map_err(InternalError::from_anyhow)?; - let email_authentication = repo - .user_email() - .lookup_authentication(email_authentication_id) - .await? - .context("Could not load the email authentication") - .map_err(InternalError::from_anyhow)?; - - // Check that the email authentication has been completed - if email_authentication.completed_at.is_none() { - return Ok(( - cookie_jar, - url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)), - ) - .into_response()); - } - - // Check that the email address isn't already used - // It is important to do that here, as we we're not checking during the - // registration, because we don't want to disclose whether an email is - // already being used or not before we verified it - if repo - .user_email() - .count(UserEmailFilter::new().for_email(&email_authentication.email)) - .await? - > 0 + // If there is an email authentication, we need to check that the email + // address was verified. If there is no email authentication attached, we + // need to make sure the server doesn't require it + let email_authentication = if let Some(email_authentication_id) = + registration.email_authentication_id { - let action = registration - .post_auth_action - .map(serde_json::from_value) - .transpose()?; + let email_authentication = repo + .user_email() + .lookup_authentication(email_authentication_id) + .await? + .context("Could not load the email authentication") + .map_err(InternalError::from_anyhow)?; - let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action) - .with_language(lang); + // Check that the email authentication has been completed + if email_authentication.completed_at.is_none() { + return Ok(( + cookie_jar, + url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)), + ) + .into_response()); + } - return Ok(( - cookie_jar, - Html(templates.render_register_steps_email_in_use(&ctx)?), - ) - .into_response()); - } + // Check that the email address isn't already used + // It is important to do that here, as we we're not checking during the + // registration, because we don't want to disclose whether an email is + // already being used or not before we verified it + if repo + .user_email() + .count(UserEmailFilter::new().for_email(&email_authentication.email)) + .await? + > 0 + { + let action = registration + .post_auth_action + .map(serde_json::from_value) + .transpose()?; + + let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action) + .with_language(lang); + + return Ok(( + cookie_jar, + Html(templates.render_register_steps_email_in_use(&ctx)?), + ) + .into_response()); + } + + Some(email_authentication) + } else if site_config.password_registration_email_required { + // This could only happen in theory during a configuration change + return Err(InternalError::from_anyhow(anyhow::anyhow!( + "Server requires an email address to complete the registration, but no email authentication was attached to the user registration" + ))); + } else { + None + }; // Check that the display name is set if registration.display_name.is_none() { @@ -236,9 +246,11 @@ pub(crate) async fn get( .add(&mut rng, &clock, &user, user_agent) .await?; - repo.user_email() - .add(&mut rng, &clock, &user, email_authentication.email) - .await?; + if let Some(email_authentication) = email_authentication { + repo.user_email() + .add(&mut rng, &clock, &user, email_authentication.email) + .await?; + } if let Some(password) = registration.password { let user_password = repo diff --git a/crates/iana-codegen/Cargo.toml b/crates/iana-codegen/Cargo.toml index 0f30c8c31..0a8e8669b 100644 --- a/crates/iana-codegen/Cargo.toml +++ b/crates/iana-codegen/Cargo.toml @@ -23,6 +23,7 @@ camino.workspace = true convert_case.workspace = true csv.workspace = true reqwest.workspace = true +rustls.workspace = true serde.workspace = true tokio.workspace = true tracing-subscriber.workspace = true diff --git a/crates/iana-codegen/src/generation.rs b/crates/iana-codegen/src/generation.rs index 3ee792df1..2f1d4836e 100644 --- a/crates/iana-codegen/src/generation.rs +++ b/crates/iana-codegen/src/generation.rs @@ -165,11 +165,12 @@ pub fn json_schema_impl( write!( f, r#"impl schemars::JsonSchema for {} {{ - fn schema_name() -> String {{ - "{}".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> {{ + std::borrow::Cow::Borrowed("{}") }} - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {{ + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {{ let enums = vec!["#, section.key, section.key, )?; @@ -179,20 +180,14 @@ pub fn json_schema_impl( f, r" // --- - schemars::schema::SchemaObject {{", + schemars::json_schema!({{", )?; if let Some(description) = &member.description { write!( f, - r" - metadata: Some(Box::new(schemars::schema::Metadata {{ - description: Some( - // --- - {}.to_owned(), - ), - ..Default::default() - }})),", + r#" + "description": {},"#, raw_string(description), )?; } @@ -200,34 +195,24 @@ pub fn json_schema_impl( write!( f, r#" - const_value: Some("{}".into()), - ..Default::default() - }} - .into(),"#, + "const": "{}", + }}),"#, member.value )?; } writeln!( f, - r" + r#" ]; let description = {}; - schemars::schema::SchemaObject {{ - metadata: Some(Box::new(schemars::schema::Metadata {{ - description: Some(description.to_owned()), - ..Default::default() - }})), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation {{ - any_of: Some(enums), - ..Default::default() - }})), - ..Default::default() - }} - .into() + schemars::json_schema!({{ + "description": description, + "anyOf": enums, + }}) }} -}}", +}}"#, raw_string(section.doc), ) } diff --git a/crates/iana-codegen/src/main.rs b/crates/iana-codegen/src/main.rs index cf809a0a9..d2ff734e0 100644 --- a/crates/iana-codegen/src/main.rs +++ b/crates/iana-codegen/src/main.rs @@ -189,6 +189,10 @@ async fn main() -> anyhow::Result<()> { .pretty() .init(); + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .unwrap(); + #[expect( clippy::disallowed_methods, reason = "reqwest::Client::new should be disallowed by clippy, but for the codegen it's fine" diff --git a/crates/iana/src/jose.rs b/crates/iana/src/jose.rs index 666d6cd7f..8a0434fad 100644 --- a/crates/iana/src/jose.rs +++ b/crates/iana/src/jose.rs @@ -144,248 +144,105 @@ impl serde::Serialize for JsonWebSignatureAlg { } impl schemars::JsonSchema for JsonWebSignatureAlg { - fn schema_name() -> String { - "JsonWebSignatureAlg".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebSignatureAlg") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"HMAC using SHA-256".to_owned(), - ), - ..Default::default() - })), - const_value: Some("HS256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"HMAC using SHA-256", + "const": "HS256", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"HMAC using SHA-384".to_owned(), - ), - ..Default::default() - })), - const_value: Some("HS384".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"HMAC using SHA-384", + "const": "HS384", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"HMAC using SHA-512".to_owned(), - ), - ..Default::default() - })), - const_value: Some("HS512".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"HMAC using SHA-512", + "const": "HS512", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSASSA-PKCS1-v1_5 using SHA-256".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RS256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSASSA-PKCS1-v1_5 using SHA-256", + "const": "RS256", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSASSA-PKCS1-v1_5 using SHA-384".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RS384".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSASSA-PKCS1-v1_5 using SHA-384", + "const": "RS384", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSASSA-PKCS1-v1_5 using SHA-512".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RS512".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSASSA-PKCS1-v1_5 using SHA-512", + "const": "RS512", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"ECDSA using P-256 and SHA-256".to_owned(), - ), - ..Default::default() - })), - const_value: Some("ES256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"ECDSA using P-256 and SHA-256", + "const": "ES256", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"ECDSA using P-384 and SHA-384".to_owned(), - ), - ..Default::default() - })), - const_value: Some("ES384".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"ECDSA using P-384 and SHA-384", + "const": "ES384", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"ECDSA using P-521 and SHA-512".to_owned(), - ), - ..Default::default() - })), - const_value: Some("ES512".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"ECDSA using P-521 and SHA-512", + "const": "ES512", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSASSA-PSS using SHA-256 and MGF1 with SHA-256".to_owned(), - ), - ..Default::default() - })), - const_value: Some("PS256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSASSA-PSS using SHA-256 and MGF1 with SHA-256", + "const": "PS256", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSASSA-PSS using SHA-384 and MGF1 with SHA-384".to_owned(), - ), - ..Default::default() - })), - const_value: Some("PS384".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSASSA-PSS using SHA-384 and MGF1 with SHA-384", + "const": "PS384", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSASSA-PSS using SHA-512 and MGF1 with SHA-512".to_owned(), - ), - ..Default::default() - })), - const_value: Some("PS512".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSASSA-PSS using SHA-512 and MGF1 with SHA-512", + "const": "PS512", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"No digital signature or MAC performed".to_owned(), - ), - ..Default::default() - })), - const_value: Some("none".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"No digital signature or MAC performed", + "const": "none", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"EdDSA signature algorithms".to_owned(), - ), - ..Default::default() - })), - const_value: Some("EdDSA".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"EdDSA signature algorithms", + "const": "EdDSA", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"ECDSA using secp256k1 curve and SHA-256".to_owned(), - ), - ..Default::default() - })), - const_value: Some("ES256K".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"ECDSA using secp256k1 curve and SHA-256", + "const": "ES256K", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"EdDSA using Ed25519 curve".to_owned(), - ), - ..Default::default() - })), - const_value: Some("Ed25519".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"EdDSA using Ed25519 curve", + "const": "Ed25519", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"EdDSA using Ed448 curve".to_owned(), - ), - ..Default::default() - })), - const_value: Some("Ed448".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"EdDSA using Ed448 curve", + "const": "Ed448", + }), ]; let description = r#"JSON Web Signature "alg" parameter"#; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -532,274 +389,115 @@ impl serde::Serialize for JsonWebEncryptionAlg { } impl schemars::JsonSchema for JsonWebEncryptionAlg { - fn schema_name() -> String { - "JsonWebEncryptionAlg".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebEncryptionAlg") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSAES-PKCS1-v1_5".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RSA1_5".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSAES-PKCS1-v1_5", + "const": "RSA1_5", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSAES OAEP using default parameters".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RSA-OAEP".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSAES OAEP using default parameters", + "const": "RSA-OAEP", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSAES OAEP using SHA-256 and MGF1 with SHA-256".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RSA-OAEP-256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSAES OAEP using SHA-256 and MGF1 with SHA-256", + "const": "RSA-OAEP-256", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES Key Wrap using 128-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A128KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES Key Wrap using 128-bit key", + "const": "A128KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES Key Wrap using 192-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A192KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES Key Wrap using 192-bit key", + "const": "A192KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES Key Wrap using 256-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A256KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES Key Wrap using 256-bit key", + "const": "A256KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Direct use of a shared symmetric key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("dir".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Direct use of a shared symmetric key", + "const": "dir", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"ECDH-ES using Concat KDF".to_owned(), - ), - ..Default::default() - })), - const_value: Some("ECDH-ES".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"ECDH-ES using Concat KDF", + "const": "ECDH-ES", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r#"ECDH-ES using Concat KDF and "A128KW" wrapping"#.to_owned(), - ), - ..Default::default() - })), - const_value: Some("ECDH-ES+A128KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r#"ECDH-ES using Concat KDF and "A128KW" wrapping"#, + "const": "ECDH-ES+A128KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r#"ECDH-ES using Concat KDF and "A192KW" wrapping"#.to_owned(), - ), - ..Default::default() - })), - const_value: Some("ECDH-ES+A192KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r#"ECDH-ES using Concat KDF and "A192KW" wrapping"#, + "const": "ECDH-ES+A192KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r#"ECDH-ES using Concat KDF and "A256KW" wrapping"#.to_owned(), - ), - ..Default::default() - })), - const_value: Some("ECDH-ES+A256KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r#"ECDH-ES using Concat KDF and "A256KW" wrapping"#, + "const": "ECDH-ES+A256KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Key wrapping with AES GCM using 128-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A128GCMKW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Key wrapping with AES GCM using 128-bit key", + "const": "A128GCMKW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Key wrapping with AES GCM using 192-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A192GCMKW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Key wrapping with AES GCM using 192-bit key", + "const": "A192GCMKW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Key wrapping with AES GCM using 256-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A256GCMKW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Key wrapping with AES GCM using 256-bit key", + "const": "A256GCMKW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r#"PBES2 with HMAC SHA-256 and "A128KW" wrapping"#.to_owned(), - ), - ..Default::default() - })), - const_value: Some("PBES2-HS256+A128KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r#"PBES2 with HMAC SHA-256 and "A128KW" wrapping"#, + "const": "PBES2-HS256+A128KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r#"PBES2 with HMAC SHA-384 and "A192KW" wrapping"#.to_owned(), - ), - ..Default::default() - })), - const_value: Some("PBES2-HS384+A192KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r#"PBES2 with HMAC SHA-384 and "A192KW" wrapping"#, + "const": "PBES2-HS384+A192KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r#"PBES2 with HMAC SHA-512 and "A256KW" wrapping"#.to_owned(), - ), - ..Default::default() - })), - const_value: Some("PBES2-HS512+A256KW".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r#"PBES2 with HMAC SHA-512 and "A256KW" wrapping"#, + "const": "PBES2-HS512+A256KW", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSA-OAEP using SHA-384 and MGF1 with SHA-384".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RSA-OAEP-384".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSA-OAEP using SHA-384 and MGF1 with SHA-384", + "const": "RSA-OAEP-384", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSA-OAEP using SHA-512 and MGF1 with SHA-512".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RSA-OAEP-512".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSA-OAEP using SHA-512 and MGF1 with SHA-512", + "const": "RSA-OAEP-512", + }), ]; let description = r#"JSON Web Encryption "alg" parameter"#; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -881,105 +579,50 @@ impl serde::Serialize for JsonWebEncryptionEnc { } impl schemars::JsonSchema for JsonWebEncryptionEnc { - fn schema_name() -> String { - "JsonWebEncryptionEnc".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebEncryptionEnc") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A128CBC-HS256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm", + "const": "A128CBC-HS256", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A192CBC-HS384".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm", + "const": "A192CBC-HS384", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A256CBC-HS512".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm", + "const": "A256CBC-HS512", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES GCM using 128-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A128GCM".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES GCM using 128-bit key", + "const": "A128GCM", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES GCM using 192-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A192GCM".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES GCM using 192-bit key", + "const": "A192GCM", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"AES GCM using 256-bit key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("A256GCM".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"AES GCM using 256-bit key", + "const": "A256GCM", + }), ]; let description = r#"JSON Web Encryption "enc" parameter"#; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -1036,40 +679,25 @@ impl serde::Serialize for JsonWebEncryptionCompressionAlgorithm { } impl schemars::JsonSchema for JsonWebEncryptionCompressionAlgorithm { - fn schema_name() -> String { - "JsonWebEncryptionCompressionAlgorithm".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebEncryptionCompressionAlgorithm") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"DEFLATE".to_owned(), - ), - ..Default::default() - })), - const_value: Some("DEF".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"DEFLATE", + "const": "DEF", + }), ]; let description = r"JSON Web Encryption Compression Algorithm"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -1141,79 +769,40 @@ impl serde::Serialize for JsonWebKeyType { } impl schemars::JsonSchema for JsonWebKeyType { - fn schema_name() -> String { - "JsonWebKeyType".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebKeyType") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Elliptic Curve".to_owned(), - ), - ..Default::default() - })), - const_value: Some("EC".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Elliptic Curve", + "const": "EC", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"RSA".to_owned(), - ), - ..Default::default() - })), - const_value: Some("RSA".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"RSA", + "const": "RSA", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Octet sequence".to_owned(), - ), - ..Default::default() - })), - const_value: Some("oct".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Octet sequence", + "const": "oct", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Octet string key pairs".to_owned(), - ), - ..Default::default() - })), - const_value: Some("OKP".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Octet string key pairs", + "const": "OKP", + }), ]; let description = r"JSON Web Key Type"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -1285,79 +874,40 @@ impl serde::Serialize for JsonWebKeyEcEllipticCurve { } impl schemars::JsonSchema for JsonWebKeyEcEllipticCurve { - fn schema_name() -> String { - "JsonWebKeyEcEllipticCurve".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebKeyEcEllipticCurve") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"P-256 Curve".to_owned(), - ), - ..Default::default() - })), - const_value: Some("P-256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"P-256 Curve", + "const": "P-256", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"P-384 Curve".to_owned(), - ), - ..Default::default() - })), - const_value: Some("P-384".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"P-384 Curve", + "const": "P-384", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"P-521 Curve".to_owned(), - ), - ..Default::default() - })), - const_value: Some("P-521".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"P-521 Curve", + "const": "P-521", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"SECG secp256k1 curve".to_owned(), - ), - ..Default::default() - })), - const_value: Some("secp256k1".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"SECG secp256k1 curve", + "const": "secp256k1", + }), ]; let description = r"JSON Web Key EC Elliptic Curve"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -1429,79 +979,40 @@ impl serde::Serialize for JsonWebKeyOkpEllipticCurve { } impl schemars::JsonSchema for JsonWebKeyOkpEllipticCurve { - fn schema_name() -> String { - "JsonWebKeyOkpEllipticCurve".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebKeyOkpEllipticCurve") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Ed25519 signature algorithm key pairs".to_owned(), - ), - ..Default::default() - })), - const_value: Some("Ed25519".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Ed25519 signature algorithm key pairs", + "const": "Ed25519", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Ed448 signature algorithm key pairs".to_owned(), - ), - ..Default::default() - })), - const_value: Some("Ed448".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Ed448 signature algorithm key pairs", + "const": "Ed448", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"X25519 function key pairs".to_owned(), - ), - ..Default::default() - })), - const_value: Some("X25519".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"X25519 function key pairs", + "const": "X25519", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"X448 function key pairs".to_owned(), - ), - ..Default::default() - })), - const_value: Some("X448".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"X448 function key pairs", + "const": "X448", + }), ]; let description = r"JSON Web Key OKP Elliptic Curve"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -1563,53 +1074,30 @@ impl serde::Serialize for JsonWebKeyUse { } impl schemars::JsonSchema for JsonWebKeyUse { - fn schema_name() -> String { - "JsonWebKeyUse".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebKeyUse") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Digital Signature or MAC".to_owned(), - ), - ..Default::default() - })), - const_value: Some("sig".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Digital Signature or MAC", + "const": "sig", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Encryption".to_owned(), - ), - ..Default::default() - })), - const_value: Some("enc".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Encryption", + "const": "enc", + }), ]; let description = r"JSON Web Key Use"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -1701,130 +1189,59 @@ impl serde::Serialize for JsonWebKeyOperation { } impl schemars::JsonSchema for JsonWebKeyOperation { - fn schema_name() -> String { - "JsonWebKeyOperation".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("JsonWebKeyOperation") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Compute digital signature or MAC".to_owned(), - ), - ..Default::default() - })), - const_value: Some("sign".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Compute digital signature or MAC", + "const": "sign", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Verify digital signature or MAC".to_owned(), - ), - ..Default::default() - })), - const_value: Some("verify".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Verify digital signature or MAC", + "const": "verify", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Encrypt content".to_owned(), - ), - ..Default::default() - })), - const_value: Some("encrypt".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Encrypt content", + "const": "encrypt", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Decrypt content and validate decryption, if applicable".to_owned(), - ), - ..Default::default() - })), - const_value: Some("decrypt".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Decrypt content and validate decryption, if applicable", + "const": "decrypt", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Encrypt key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("wrapKey".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Encrypt key", + "const": "wrapKey", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Decrypt key and validate decryption, if applicable".to_owned(), - ), - ..Default::default() - })), - const_value: Some("unwrapKey".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Decrypt key and validate decryption, if applicable", + "const": "unwrapKey", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Derive key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("deriveKey".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Derive key", + "const": "deriveKey", + }), // --- - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some( - // --- - r"Derive bits not to be used as a key".to_owned(), - ), - ..Default::default() - })), - const_value: Some("deriveBits".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "description": r"Derive bits not to be used as a key", + "const": "deriveBits", + }), ]; let description = r"JSON Web Key Operation"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } diff --git a/crates/iana/src/oauth.rs b/crates/iana/src/oauth.rs index 3b65ce9f2..f59cd7c8c 100644 --- a/crates/iana/src/oauth.rs +++ b/crates/iana/src/oauth.rs @@ -79,51 +79,36 @@ impl serde::Serialize for OAuthAccessTokenType { } impl schemars::JsonSchema for OAuthAccessTokenType { - fn schema_name() -> String { - "OAuthAccessTokenType".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("OAuthAccessTokenType") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - const_value: Some("Bearer".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "Bearer", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("N_A".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "N_A", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("PoP".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "PoP", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("DPoP".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "DPoP", + }), ]; let description = r"OAuth Access Token Type"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -210,75 +195,52 @@ impl serde::Serialize for OAuthAuthorizationEndpointResponseType { } impl schemars::JsonSchema for OAuthAuthorizationEndpointResponseType { - fn schema_name() -> String { - "OAuthAuthorizationEndpointResponseType".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("OAuthAuthorizationEndpointResponseType") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - const_value: Some("code".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "code", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("code id_token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "code id_token", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("code id_token token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "code id_token token", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("code token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "code token", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("id_token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "id_token", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("id_token token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "id_token token", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("none".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "none", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "token", + }), ]; let description = r"OAuth Authorization Endpoint Response Type"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -345,45 +307,32 @@ impl serde::Serialize for OAuthTokenTypeHint { } impl schemars::JsonSchema for OAuthTokenTypeHint { - fn schema_name() -> String { - "OAuthTokenTypeHint".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("OAuthTokenTypeHint") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - const_value: Some("access_token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "access_token", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("refresh_token".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "refresh_token", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("pct".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "pct", + }), ]; let description = r"OAuth Token Type Hint"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -470,69 +419,48 @@ impl serde::Serialize for OAuthClientAuthenticationMethod { } impl schemars::JsonSchema for OAuthClientAuthenticationMethod { - fn schema_name() -> String { - "OAuthClientAuthenticationMethod".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("OAuthClientAuthenticationMethod") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - const_value: Some("none".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "none", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("client_secret_post".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "client_secret_post", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("client_secret_basic".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "client_secret_basic", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("client_secret_jwt".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "client_secret_jwt", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("private_key_jwt".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "private_key_jwt", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("tls_client_auth".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "tls_client_auth", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("self_signed_tls_client_auth".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "self_signed_tls_client_auth", + }), ]; let description = r"OAuth Token Endpoint Authentication Method"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } @@ -594,38 +522,27 @@ impl serde::Serialize for PkceCodeChallengeMethod { } impl schemars::JsonSchema for PkceCodeChallengeMethod { - fn schema_name() -> String { - "PkceCodeChallengeMethod".to_owned() + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("PkceCodeChallengeMethod") } - fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + #[allow(clippy::too_many_lines)] + fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema { let enums = vec![ // --- - schemars::schema::SchemaObject { - const_value: Some("plain".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "plain", + }), // --- - schemars::schema::SchemaObject { - const_value: Some("S256".into()), - ..Default::default() - } - .into(), + schemars::json_schema!({ + "const": "S256", + }), ]; let description = r"PKCE Code Challenge Method"; - schemars::schema::SchemaObject { - metadata: Some(Box::new(schemars::schema::Metadata { - description: Some(description.to_owned()), - ..Default::default() - })), - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(enums), - ..Default::default() - })), - ..Default::default() - } - .into() + schemars::json_schema!({ + "description": description, + "anyOf": enums, + }) } } diff --git a/crates/oauth2-types/src/oidc.rs b/crates/oauth2-types/src/oidc.rs index 39e1074b1..9ac67278b 100644 --- a/crates/oauth2-types/src/oidc.rs +++ b/crates/oauth2-types/src/oidc.rs @@ -692,10 +692,6 @@ impl ProviderMetadata { .token_endpoint_auth_signing_alg_values_supported .iter() .flatten(), - metadata - .token_endpoint_auth_methods_supported - .iter() - .flatten(), )?; if let Some(url) = &metadata.revocation_endpoint { @@ -708,33 +704,18 @@ impl ProviderMetadata { .revocation_endpoint_auth_signing_alg_values_supported .iter() .flatten(), - metadata - .revocation_endpoint_auth_methods_supported - .iter() - .flatten(), )?; if let Some(url) = &metadata.introspection_endpoint { validate_url("introspection_endpoint", url, ExtraUrlRestrictions::None)?; } - // The list can also contain token types so remove them as we don't need to - // check them. - let introspection_methods = metadata - .introspection_endpoint_auth_methods_supported - .as_ref() - .map(|v| { - v.iter() - .filter_map(AuthenticationMethodOrAccessTokenType::authentication_method) - .collect::>() - }); validate_signing_alg_values_supported( "introspection_endpoint", metadata .introspection_endpoint_auth_signing_alg_values_supported .iter() .flatten(), - introspection_methods.into_iter().flatten(), )?; if let Some(url) = &metadata.userinfo_endpoint { @@ -1099,12 +1080,6 @@ pub enum ProviderMetadataVerificationError { #[error("missing `implicit` grant type")] GrantTypesMissingImplicit, - /// The given endpoint is missing auth signing algorithm values, but they - /// are required because it supports at least one of the `client_secret_jwt` - /// or `private_key_jwt` authentication methods. - #[error("{0} missing auth signing algorithm values")] - MissingAuthSigningAlgValues(&'static str), - /// `none` is in the given endpoint's signing algorithm values, but is not /// allowed. #[error("{0} signing algorithm values contain `none`")] @@ -1176,32 +1151,14 @@ fn validate_url( fn validate_signing_alg_values_supported<'a>( endpoint: &'static str, values: impl Iterator, - mut methods: impl Iterator, ) -> Result<(), ProviderMetadataVerificationError> { - let mut no_values = true; - for value in values { if *value == JsonWebSignatureAlg::None { return Err(ProviderMetadataVerificationError::SigningAlgValuesWithNone( endpoint, )); } - - no_values = false; } - - if no_values - && methods.any(|method| { - matches!( - method, - OAuthClientAuthenticationMethod::ClientSecretJwt - | OAuthClientAuthenticationMethod::PrivateKeyJwt - ) - }) - { - return Err(ProviderMetadataVerificationError::MissingAuthSigningAlgValues(endpoint)); - } - Ok(()) } @@ -1543,34 +1500,30 @@ mod tests { Some(vec![JsonWebSignatureAlg::Rs256, JsonWebSignatureAlg::EdDsa]); metadata.clone().validate(&issuer).unwrap(); - // Err - `client_secret_jwt` without signing alg values. + // Ok - `client_secret_jwt` with signing alg values. metadata.token_endpoint_auth_methods_supported = Some(vec![OAuthClientAuthenticationMethod::ClientSecretJwt]); - metadata.token_endpoint_auth_signing_alg_values_supported = None; - let endpoint = assert_matches!( - metadata.clone().validate(&issuer), - Err(ProviderMetadataVerificationError::MissingAuthSigningAlgValues(endpoint)) => endpoint - ); - assert_eq!(endpoint, "token_endpoint"); - - // Ok - `client_secret_jwt` with signing alg values. metadata.token_endpoint_auth_signing_alg_values_supported = Some(vec![JsonWebSignatureAlg::Rs256]); metadata.clone().validate(&issuer).unwrap(); - // Err - `private_key_jwt` without signing alg values. + // Ok - `private_key_jwt` with signing alg values. + metadata.token_endpoint_auth_methods_supported = + Some(vec![OAuthClientAuthenticationMethod::PrivateKeyJwt]); + metadata.token_endpoint_auth_signing_alg_values_supported = + Some(vec![JsonWebSignatureAlg::Rs256]); + metadata.clone().validate(&issuer).unwrap(); + + // Ok - `client_secret_jwt` without signing alg values. + metadata.token_endpoint_auth_methods_supported = + Some(vec![OAuthClientAuthenticationMethod::ClientSecretJwt]); + metadata.token_endpoint_auth_signing_alg_values_supported = None; + metadata.clone().validate(&issuer).unwrap(); + + // Ok - `private_key_jwt` without signing alg values. metadata.token_endpoint_auth_methods_supported = Some(vec![OAuthClientAuthenticationMethod::PrivateKeyJwt]); metadata.token_endpoint_auth_signing_alg_values_supported = None; - let endpoint = assert_matches!( - metadata.clone().validate(&issuer), - Err(ProviderMetadataVerificationError::MissingAuthSigningAlgValues(endpoint)) => endpoint - ); - assert_eq!(endpoint, "token_endpoint"); - - // Ok - `private_key_jwt` with signing alg values. - metadata.token_endpoint_auth_signing_alg_values_supported = - Some(vec![JsonWebSignatureAlg::Rs256]); metadata.clone().validate(&issuer).unwrap(); // Ok - Other auth methods without signing alg values. diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index ac0770411..4c9f1117d 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -86,11 +86,13 @@ impl core::str::FromStr for ResponseMode { Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, SerializeDisplay, DeserializeFromStr, )] #[non_exhaustive] +#[derive(Default)] pub enum Display { /// The Authorization Server should display the authentication and consent /// UI consistent with a full User Agent page view. /// /// This is the default display mode. + #[default] Page, /// The Authorization Server should display the authentication and consent @@ -135,12 +137,6 @@ impl core::str::FromStr for Display { } } -impl Default for Display { - fn default() -> Self { - Self::Page - } -} - /// Value that specifies whether the Authorization Server prompts the End-User /// for reauthentication and consent. /// @@ -807,6 +803,7 @@ pub struct IntrospectionResponse { pub jti: Option, /// MAS extension: explicit device ID + /// Only used for compatibility access and refresh tokens. pub device_id: Option, } diff --git a/crates/policy/src/bin/schema.rs b/crates/policy/src/bin/schema.rs index cc908d4c9..8e9c81a07 100644 --- a/crates/policy/src/bin/schema.rs +++ b/crates/policy/src/bin/schema.rs @@ -14,7 +14,7 @@ use std::path::{Path, PathBuf}; use mas_policy::model::{ AuthorizationGrantInput, ClientRegistrationInput, EmailInput, RegisterInput, }; -use schemars::{JsonSchema, r#gen::SchemaSettings}; +use schemars::{JsonSchema, generate::SchemaSettings}; fn write_schema(out_dir: Option<&Path>, file: &str) { let mut writer: Box = if let Some(out_dir) = out_dir { @@ -27,11 +27,7 @@ fn write_schema(out_dir: Option<&Path>, file: &str) { Box::new(std::io::stdout()) }; - let settings = SchemaSettings::draft07().with(|s| { - s.option_nullable = false; - s.option_add_null_type = false; - }); - let generator = settings.into_generator(); + let generator = SchemaSettings::draft07().into_generator(); let schema = generator.into_root_schema_for::(); serde_json::to_writer_pretty(&mut writer, &schema).expect("Failed to serialize schema"); writer.flush().expect("Failed to flush writer"); diff --git a/crates/spa/src/vite.rs b/crates/spa/src/vite.rs index e2706d512..b488bea6b 100644 --- a/crates/spa/src/vite.rs +++ b/crates/spa/src/vite.rs @@ -12,10 +12,13 @@ use thiserror::Error; #[derive(serde::Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ManifestEntry { - #[allow(dead_code)] + #[expect(dead_code)] name: Option, - #[allow(dead_code)] + #[expect(dead_code)] + names: Option>, + + #[expect(dead_code)] src: Option, file: Utf8PathBuf, @@ -24,15 +27,15 @@ pub struct ManifestEntry { assets: Option>, - #[allow(dead_code)] + #[expect(dead_code)] is_entry: Option, - #[allow(dead_code)] + #[expect(dead_code)] is_dynamic_entry: Option, imports: Option>, - #[allow(dead_code)] + #[expect(dead_code)] dynamic_imports: Option>, integrity: Option, diff --git a/crates/storage-pg/.sqlx/query-06d67595eeef23d5f2773632e0956577d98074e244a35c0d3be24bc18d9d0daa.json b/crates/storage-pg/.sqlx/query-06d67595eeef23d5f2773632e0956577d98074e244a35c0d3be24bc18d9d0daa.json new file mode 100644 index 000000000..55509569c --- /dev/null +++ b/crates/storage-pg/.sqlx/query-06d67595eeef23d5f2773632e0956577d98074e244a35c0d3be24bc18d9d0daa.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE personal_sessions\n SET revoked_at = $2\n WHERE personal_session_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "06d67595eeef23d5f2773632e0956577d98074e244a35c0d3be24bc18d9d0daa" +} diff --git a/crates/storage-pg/.sqlx/query-0e45995714e60b71e0f0158500a63aa46225245a04d1c7bc24b5275c44a6d58d.json b/crates/storage-pg/.sqlx/query-0e45995714e60b71e0f0158500a63aa46225245a04d1c7bc24b5275c44a6d58d.json new file mode 100644 index 000000000..5bba6548d --- /dev/null +++ b/crates/storage-pg/.sqlx/query-0e45995714e60b71e0f0158500a63aa46225245a04d1c7bc24b5275c44a6d58d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE personal_access_tokens\n SET revoked_at = $2\n WHERE personal_access_token_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "0e45995714e60b71e0f0158500a63aa46225245a04d1c7bc24b5275c44a6d58d" +} diff --git a/crates/storage-pg/.sqlx/query-109f0c859e123966462f1001aef550e4e12d1778474aba72762d9aa093d21ee2.json b/crates/storage-pg/.sqlx/query-109f0c859e123966462f1001aef550e4e12d1778474aba72762d9aa093d21ee2.json new file mode 100644 index 000000000..83400921a --- /dev/null +++ b/crates/storage-pg/.sqlx/query-109f0c859e123966462f1001aef550e4e12d1778474aba72762d9aa093d21ee2.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO personal_sessions\n ( personal_session_id\n , owner_user_id\n , owner_oauth2_client_id\n , actor_user_id\n , human_name\n , scope_list\n , created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Uuid", + "Text", + "TextArray", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "109f0c859e123966462f1001aef550e4e12d1778474aba72762d9aa093d21ee2" +} diff --git a/crates/storage-pg/.sqlx/query-d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716.json b/crates/storage-pg/.sqlx/query-23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08.json similarity index 74% rename from crates/storage-pg/.sqlx/query-d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716.json rename to crates/storage-pg/.sqlx/query-23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08.json index 171a83623..28391d844 100644 --- a/crates/storage-pg/.sqlx/query-d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716.json +++ b/crates/storage-pg/.sqlx/query-23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE LOWER(username) = LOWER($1)\n ", + "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n , is_guest\n FROM users\n WHERE LOWER(username) = LOWER($1)\n ", "describe": { "columns": [ { @@ -32,6 +32,11 @@ "ordinal": 5, "name": "can_request_admin", "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "is_guest", + "type_info": "Bool" } ], "parameters": { @@ -45,8 +50,9 @@ false, true, true, + false, false ] }, - "hash": "d2a4f5c01603463b78198529d295f7f121769ea5730d01c20c0ddbcdc79a5716" + "hash": "23d5fcd8bf611dc7279bef0d66ce05461c3c1f43f966fee3a80ae42540783f08" } diff --git a/crates/storage-pg/.sqlx/query-2a61003da3655158e6a261d91fdff670f1b4ba3c56605c53e2b905d7ec38c8be.json b/crates/storage-pg/.sqlx/query-2a61003da3655158e6a261d91fdff670f1b4ba3c56605c53e2b905d7ec38c8be.json new file mode 100644 index 000000000..21a67060b --- /dev/null +++ b/crates/storage-pg/.sqlx/query-2a61003da3655158e6a261d91fdff670f1b4ba3c56605c53e2b905d7ec38c8be.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM personal_access_tokens\n WHERE personal_session_id IN (\n SELECT personal_session_id\n FROM personal_sessions\n WHERE owner_oauth2_client_id = $1\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "2a61003da3655158e6a261d91fdff670f1b4ba3c56605c53e2b905d7ec38c8be" +} diff --git a/crates/storage-pg/.sqlx/query-cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d.json b/crates/storage-pg/.sqlx/query-4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce.json similarity index 75% rename from crates/storage-pg/.sqlx/query-cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d.json rename to crates/storage-pg/.sqlx/query-4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce.json index 6603fa37d..6c8cdbe88 100644 --- a/crates/storage-pg/.sqlx/query-cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d.json +++ b/crates/storage-pg/.sqlx/query-4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE user_id = $1\n ", + "query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n , is_guest\n FROM users\n WHERE user_id = $1\n ", "describe": { "columns": [ { @@ -32,6 +32,11 @@ "ordinal": 5, "name": "can_request_admin", "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "is_guest", + "type_info": "Bool" } ], "parameters": { @@ -45,8 +50,9 @@ false, true, true, + false, false ] }, - "hash": "cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d" + "hash": "4dad1838536c10ba723adc0fb6da0f24afb3d6a1925a80a1b6d35b9a8258a0ce" } diff --git a/crates/storage-pg/.sqlx/query-f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8.json b/crates/storage-pg/.sqlx/query-572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40.json similarity index 84% rename from crates/storage-pg/.sqlx/query-f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8.json rename to crates/storage-pg/.sqlx/query-572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40.json index 1ddb0acc8..155277181 100644 --- a/crates/storage-pg/.sqlx/query-f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8.json +++ b/crates/storage-pg/.sqlx/query-572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , s.user_agent AS \"user_session_user_agent\"\n , s.last_active_at AS \"user_session_last_active_at\"\n , s.last_active_ip AS \"user_session_last_active_ip: IpAddr\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n , u.deactivated_at AS \"user_deactivated_at\"\n , u.can_request_admin AS \"user_can_request_admin\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", + "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , s.user_agent AS \"user_session_user_agent\"\n , s.last_active_at AS \"user_session_last_active_at\"\n , s.last_active_ip AS \"user_session_last_active_ip: IpAddr\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n , u.deactivated_at AS \"user_deactivated_at\"\n , u.can_request_admin AS \"user_can_request_admin\"\n , u.is_guest AS \"user_is_guest\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", "describe": { "columns": [ { @@ -62,6 +62,11 @@ "ordinal": 11, "name": "user_can_request_admin", "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "user_is_guest", + "type_info": "Bool" } ], "parameters": { @@ -81,8 +86,9 @@ false, true, true, + false, false ] }, - "hash": "f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8" + "hash": "572ead41d62cfbe40e6f0c8edf6928e8eebd99036255b62d688ac02b5bd74b40" } diff --git a/crates/storage-pg/.sqlx/query-64b6e274e2bed6814f5ae41ddf57093589f7d1b2b8458521b635546b8012041e.json b/crates/storage-pg/.sqlx/query-64b6e274e2bed6814f5ae41ddf57093589f7d1b2b8458521b635546b8012041e.json new file mode 100644 index 000000000..6b2e85bf1 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-64b6e274e2bed6814f5ae41ddf57093589f7d1b2b8458521b635546b8012041e.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE personal_sessions\n SET last_active_at = GREATEST(t.last_active_at, personal_sessions.last_active_at)\n , last_active_ip = COALESCE(t.last_active_ip, personal_sessions.last_active_ip)\n FROM (\n SELECT *\n FROM UNNEST($1::uuid[], $2::timestamptz[], $3::inet[])\n AS t(personal_session_id, last_active_at, last_active_ip)\n ) AS t\n WHERE personal_sessions.personal_session_id = t.personal_session_id\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "TimestamptzArray", + "InetArray" + ] + }, + "nullable": [] + }, + "hash": "64b6e274e2bed6814f5ae41ddf57093589f7d1b2b8458521b635546b8012041e" +} diff --git a/crates/storage-pg/.sqlx/query-90875bdd2f75cdf0dc3f48dc2516f5c701411387c939f6b8a3478b41b3de4f20.json b/crates/storage-pg/.sqlx/query-90875bdd2f75cdf0dc3f48dc2516f5c701411387c939f6b8a3478b41b3de4f20.json new file mode 100644 index 000000000..66aab4ee6 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-90875bdd2f75cdf0dc3f48dc2516f5c701411387c939f6b8a3478b41b3de4f20.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT personal_access_token_id\n , personal_session_id\n , created_at\n , expires_at\n , revoked_at\n\n FROM personal_access_tokens\n\n WHERE access_token_sha256 = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "personal_access_token_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "personal_session_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "revoked_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Bytea" + ] + }, + "nullable": [ + false, + false, + false, + true, + true + ] + }, + "hash": "90875bdd2f75cdf0dc3f48dc2516f5c701411387c939f6b8a3478b41b3de4f20" +} diff --git a/crates/storage-pg/.sqlx/query-9e8152d445f9996b221ad3690ba982ad01035296bf4539ca5620a043924a7292.json b/crates/storage-pg/.sqlx/query-9e8152d445f9996b221ad3690ba982ad01035296bf4539ca5620a043924a7292.json new file mode 100644 index 000000000..0a838d1a5 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-9e8152d445f9996b221ad3690ba982ad01035296bf4539ca5620a043924a7292.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE personal_access_tokens\n SET revoked_at = $2\n WHERE personal_session_id = $1 AND revoked_at IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "9e8152d445f9996b221ad3690ba982ad01035296bf4539ca5620a043924a7292" +} diff --git a/crates/storage-pg/.sqlx/query-a0be6c56e470382b9470df414497e260ba8911123744980e24a52bc9b95bd056.json b/crates/storage-pg/.sqlx/query-a0be6c56e470382b9470df414497e260ba8911123744980e24a52bc9b95bd056.json new file mode 100644 index 000000000..3542f8481 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-a0be6c56e470382b9470df414497e260ba8911123744980e24a52bc9b95bd056.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO personal_access_tokens\n (personal_access_token_id, personal_session_id, access_token_sha256, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Bytea", + "Timestamptz", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "a0be6c56e470382b9470df414497e260ba8911123744980e24a52bc9b95bd056" +} diff --git a/crates/storage-pg/.sqlx/query-d02248136aa6b27636814dee4e0bc38395ab6c6fdf979616fa16fc490897cee3.json b/crates/storage-pg/.sqlx/query-d02248136aa6b27636814dee4e0bc38395ab6c6fdf979616fa16fc490897cee3.json new file mode 100644 index 000000000..99df8e139 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-d02248136aa6b27636814dee4e0bc38395ab6c6fdf979616fa16fc490897cee3.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT personal_access_token_id\n , personal_session_id\n , created_at\n , expires_at\n , revoked_at\n\n FROM personal_access_tokens\n\n WHERE personal_session_id = $1\n AND revoked_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "personal_access_token_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "personal_session_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "revoked_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + true + ] + }, + "hash": "d02248136aa6b27636814dee4e0bc38395ab6c6fdf979616fa16fc490897cee3" +} diff --git a/crates/storage-pg/.sqlx/query-dca9b361c4409b14498b85f192b0034201575a49e0240ac6715b55ad8d381d0e.json b/crates/storage-pg/.sqlx/query-dca9b361c4409b14498b85f192b0034201575a49e0240ac6715b55ad8d381d0e.json new file mode 100644 index 000000000..39447cd10 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-dca9b361c4409b14498b85f192b0034201575a49e0240ac6715b55ad8d381d0e.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM personal_sessions\n WHERE owner_oauth2_client_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "dca9b361c4409b14498b85f192b0034201575a49e0240ac6715b55ad8d381d0e" +} diff --git a/crates/storage-pg/.sqlx/query-e1746b33c2f0d10f26332195f78e1ef2f192ca66f8000d1385626154e5ce4f7e.json b/crates/storage-pg/.sqlx/query-e1746b33c2f0d10f26332195f78e1ef2f192ca66f8000d1385626154e5ce4f7e.json new file mode 100644 index 000000000..2112e7603 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-e1746b33c2f0d10f26332195f78e1ef2f192ca66f8000d1385626154e5ce4f7e.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT personal_access_token_id\n , personal_session_id\n , created_at\n , expires_at\n , revoked_at\n\n FROM personal_access_tokens\n\n WHERE personal_access_token_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "personal_access_token_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "personal_session_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "revoked_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + true + ] + }, + "hash": "e1746b33c2f0d10f26332195f78e1ef2f192ca66f8000d1385626154e5ce4f7e" +} diff --git a/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json b/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json index f5503fa0e..ef1ac0372 100644 --- a/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json +++ b/crates/storage-pg/.sqlx/query-fcd8b4b9e003d1540357c6bf1ff9c715560d011d4c01112703a9c046170c84f1.json @@ -23,7 +23,7 @@ "Left": [] }, "nullable": [ - false, + true, true, null ] diff --git a/crates/storage-pg/.sqlx/query-fd32368fa6cd16a9704cdea54f7729681d450669563dd1178c492ffce51e5ff2.json b/crates/storage-pg/.sqlx/query-fd32368fa6cd16a9704cdea54f7729681d450669563dd1178c492ffce51e5ff2.json new file mode 100644 index 000000000..b46904ccb --- /dev/null +++ b/crates/storage-pg/.sqlx/query-fd32368fa6cd16a9704cdea54f7729681d450669563dd1178c492ffce51e5ff2.json @@ -0,0 +1,76 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT personal_session_id\n , owner_user_id\n , owner_oauth2_client_id\n , actor_user_id\n , scope_list\n , created_at\n , revoked_at\n , human_name\n , last_active_at\n , last_active_ip as \"last_active_ip: IpAddr\"\n FROM personal_sessions\n\n WHERE personal_session_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "personal_session_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "owner_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "owner_oauth2_client_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "actor_user_id", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "scope_list", + "type_info": "TextArray" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "revoked_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "human_name", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "last_active_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "last_active_ip: IpAddr", + "type_info": "Inet" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + true, + true, + false, + false, + false, + true, + false, + true, + true + ] + }, + "hash": "fd32368fa6cd16a9704cdea54f7729681d450669563dd1178c492ffce51e5ff2" +} diff --git a/crates/storage-pg/Cargo.toml b/crates/storage-pg/Cargo.toml index 149e92fc6..8710ead70 100644 --- a/crates/storage-pg/Cargo.toml +++ b/crates/storage-pg/Cargo.toml @@ -27,6 +27,7 @@ rand.workspace = true sea-query-binder.workspace = true sea-query.workspace = true serde_json.workspace = true +sha2.workspace = true sqlx.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/storage-pg/migrations/20250915092000_pgtrgm_extension.sql b/crates/storage-pg/migrations/20250915092000_pgtrgm_extension.sql new file mode 100644 index 000000000..2ebc26d25 --- /dev/null +++ b/crates/storage-pg/migrations/20250915092000_pgtrgm_extension.sql @@ -0,0 +1,10 @@ +-- no-transaction +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- This enables the pg_trgm extension, which is used for search filters +-- Starting Posgres 16, this extension is marked as "trusted", meaning it can be +-- installed by non-superusers +CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/crates/storage-pg/migrations/20250915092635_users_username_trgm_idx.sql b/crates/storage-pg/migrations/20250915092635_users_username_trgm_idx.sql new file mode 100644 index 000000000..5f007d750 --- /dev/null +++ b/crates/storage-pg/migrations/20250915092635_users_username_trgm_idx.sql @@ -0,0 +1,10 @@ +-- no-transaction +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- This adds an index on the username field for ILIKE '%search%' operations, +-- enabling fuzzy searches of usernames +CREATE INDEX CONCURRENTLY users_username_trgm_idx + ON users USING gin(username gin_trgm_ops); diff --git a/crates/storage-pg/migrations/20250924132713_personal_access_tokens.sql b/crates/storage-pg/migrations/20250924132713_personal_access_tokens.sql new file mode 100644 index 000000000..0e113b156 --- /dev/null +++ b/crates/storage-pg/migrations/20250924132713_personal_access_tokens.sql @@ -0,0 +1,68 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- A family of personal access tokens. This is a long-lived wrapper around the personal access tokens +-- themselves, allowing tokens to be regenerated whilst still retaining a persistent identifier for them. +CREATE TABLE personal_sessions ( + personal_session_id UUID NOT NULL PRIMARY KEY, + + -- If this session is owned by a user, the ID of the user. + -- Null otherwise. + owner_user_id UUID REFERENCES users(user_id), + + -- If this session is owned by an OAuth 2 Client (via Client Credentials grant), + -- the ID of the owning client. + -- Null otherwise. + owner_oauth2_client_id UUID REFERENCES oauth2_clients(oauth2_client_id), + + actor_user_id UUID NOT NULL REFERENCES users(user_id), + -- A human-readable label, intended to describe what the session is for. + human_name TEXT NOT NULL, + -- The OAuth2 scopes for the session, identical to OAuth2 sessions. + -- May include a device ID, but this is optional (sessions can be deviceless). + scope_list TEXT[] NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + -- If set, none of the tokens will be valid anymore. + revoked_at TIMESTAMP WITH TIME ZONE, + last_active_at TIMESTAMP WITH TIME ZONE, + last_active_ip INET, + + -- There must be exactly one owner. + CONSTRAINT personal_sessions_exactly_one_owner CHECK ((owner_user_id IS NULL) <> (owner_oauth2_client_id IS NULL)) +); + +-- Individual tokens. +CREATE TABLE personal_access_tokens ( + personal_access_token_id UUID NOT NULL PRIMARY KEY, + -- The session this access token belongs to. + personal_session_id UUID NOT NULL REFERENCES personal_sessions(personal_session_id), + -- SHA256 of the access token. + -- This is a lightweight measure to stop a database backup (or other + -- unauthorised read-only database access) escalating into real permissions + -- on a live system. + -- We could have used a hash with secret key, but this would no longer be + -- 'free' protection because it would need configuration (and introduce + -- potential issues with configuring it wrong). + -- This is currently inconsistent with other access token tables but it would + -- make sense to migrate those to match in the future. + access_token_sha256 BYTEA NOT NULL UNIQUE + -- A SHA256 hash is 32 bytes long + CHECK (octet_length(access_token_sha256) = 32), + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + -- If set, the token won't be valid after this time. + -- If not set, the token never automatically expires. + expires_at TIMESTAMP WITH TIME ZONE, + -- If set, this token is not valid anymore. + revoked_at TIMESTAMP WITH TIME ZONE +); + +-- Ensure we can only have one active personal access token in each family. +CREATE UNIQUE INDEX ON personal_access_tokens (personal_session_id) WHERE revoked_at IS NOT NULL; + +-- Add indices to satisfy foreign key backward checks +-- (and likely filter queries) +CREATE INDEX ON personal_sessions (owner_user_id) WHERE owner_user_id IS NOT NULL; +CREATE INDEX ON personal_sessions (owner_oauth2_client_id) WHERE owner_oauth2_client_id IS NOT NULL; +CREATE INDEX ON personal_sessions (actor_user_id); diff --git a/crates/storage-pg/migrations/20251023134634_personal_access_tokens_unique_fix.sql b/crates/storage-pg/migrations/20251023134634_personal_access_tokens_unique_fix.sql new file mode 100644 index 000000000..9274d16ac --- /dev/null +++ b/crates/storage-pg/migrations/20251023134634_personal_access_tokens_unique_fix.sql @@ -0,0 +1,14 @@ +-- Copyright 2025 Element Creations Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + + +-- Fix a faulty constraint. +-- The condition was incorrectly specified as `revoked_at IS NOT NULL` +-- when `revoked_at IS NULL` was meant. + +DROP INDEX personal_access_tokens_personal_session_id_idx; + +-- Ensure we can only have one active personal access token in each family. +CREATE UNIQUE INDEX ON personal_access_tokens (personal_session_id) WHERE revoked_at IS NULL; diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index 62f3979a9..4e12810cc 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -55,7 +55,9 @@ mod priv_ { use std::net::IpAddr; use chrono::{DateTime, Utc}; + use mas_storage::pagination::Node; use sea_query::enum_def; + use ulid::Ulid; use uuid::Uuid; #[derive(sqlx::FromRow)] @@ -77,6 +79,12 @@ mod priv_ { pub(super) last_active_at: Option>, pub(super) last_active_ip: Option, } + + impl Node for AppSessionLookup { + fn cursor(&self) -> Ulid { + self.cursor.into() + } + } } use priv_::{AppSessionLookup, AppSessionLookupIden}; @@ -592,13 +600,13 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); let active_list = repo.app_session().list(active, pagination).await.unwrap(); assert_eq!(active_list.edges.len(), 1); assert_eq!( - active_list.edges[0], + active_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); @@ -618,7 +626,7 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); let active_list = repo.app_session().list(active, pagination).await.unwrap(); @@ -626,7 +634,7 @@ mod tests { let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); assert_eq!(finished_list.edges.len(), 1); assert_eq!( - finished_list.edges[0], + finished_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); @@ -680,25 +688,25 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 2); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); assert_eq!( - full_list.edges[1], + full_list.edges[1].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); let active_list = repo.app_session().list(active, pagination).await.unwrap(); assert_eq!(active_list.edges.len(), 1); assert_eq!( - active_list.edges[0], + active_list.edges[0].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); assert_eq!(finished_list.edges.len(), 1); assert_eq!( - finished_list.edges[0], + finished_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); @@ -716,11 +724,11 @@ mod tests { let full_list = repo.app_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 2); assert_eq!( - full_list.edges[0], + full_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); assert_eq!( - full_list.edges[1], + full_list.edges[1].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); @@ -730,11 +738,11 @@ mod tests { let finished_list = repo.app_session().list(finished, pagination).await.unwrap(); assert_eq!(finished_list.edges.len(), 2); assert_eq!( - finished_list.edges[0], + finished_list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); assert_eq!( - full_list.edges[1], + full_list.edges[1].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); @@ -744,7 +752,7 @@ mod tests { let list = repo.app_session().list(filter, pagination).await.unwrap(); assert_eq!(list.edges.len(), 1); assert_eq!( - list.edges[0], + list.edges[0].node, AppSession::Compat(Box::new(compat_session.clone())) ); @@ -753,7 +761,7 @@ mod tests { let list = repo.app_session().list(filter, pagination).await.unwrap(); assert_eq!(list.edges.len(), 1); assert_eq!( - list.edges[0], + list.edges[0].node, AppSession::OAuth2(Box::new(oauth_session.clone())) ); diff --git a/crates/storage-pg/src/compat/mod.rs b/crates/storage-pg/src/compat/mod.rs index db190db71..d42c9b1af 100644 --- a/crates/storage-pg/src/compat/mod.rs +++ b/crates/storage-pg/src/compat/mod.rs @@ -92,14 +92,14 @@ mod tests { let full_list = repo.compat_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); - assert_eq!(full_list.edges[0].0.id, session.id); + assert_eq!(full_list.edges[0].node.0.id, session.id); let active_list = repo .compat_session() .list(active, pagination) .await .unwrap(); assert_eq!(active_list.edges.len(), 1); - assert_eq!(active_list.edges[0].0.id, session.id); + assert_eq!(active_list.edges[0].node.0.id, session.id); let finished_list = repo .compat_session() .list(finished, pagination) @@ -150,7 +150,7 @@ mod tests { .await .unwrap(); assert_eq!(list.edges.len(), 1); - let session_lookup = &list.edges[0].0; + let session_lookup = &list.edges[0].node.0; assert_eq!(session_lookup.id, session.id); assert_eq!(session_lookup.user_id, user.id); assert_eq!(session.device.as_ref().unwrap().as_str(), device_str); @@ -168,7 +168,7 @@ mod tests { let full_list = repo.compat_session().list(all, pagination).await.unwrap(); assert_eq!(full_list.edges.len(), 1); - assert_eq!(full_list.edges[0].0.id, session.id); + assert_eq!(full_list.edges[0].node.0.id, session.id); let active_list = repo .compat_session() .list(active, pagination) @@ -181,7 +181,7 @@ mod tests { .await .unwrap(); assert_eq!(finished_list.edges.len(), 1); - assert_eq!(finished_list.edges[0].0.id, session.id); + assert_eq!(finished_list.edges[0].node.0.id, session.id); // Reload the session and check again let session_lookup = repo @@ -260,14 +260,14 @@ mod tests { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].0.id, sso_login_session.id); + assert_eq!(list.edges[0].node.0.id, sso_login_session.id); let list = repo .compat_session() .list(unknown, pagination) .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].0.id, unknown_session.id); + assert_eq!(list.edges[0].node.0.id, unknown_session.id); // Check that combining the two filters works // At this point, there is one active SSO login session and one finished unknown @@ -696,7 +696,8 @@ mod tests { // List all logins let logins = repo.compat_sso_login().list(all, pagination).await.unwrap(); assert!(!logins.has_next_page); - assert_eq!(logins.edges, vec![login.clone()]); + assert_eq!(logins.edges.len(), 1); + assert_eq!(logins.edges[0].node, login); // List the logins for the user let logins = repo @@ -705,7 +706,8 @@ mod tests { .await .unwrap(); assert!(!logins.has_next_page); - assert_eq!(logins.edges, vec![login.clone()]); + assert_eq!(logins.edges.len(), 1); + assert_eq!(logins.edges[0].node, login); // List only the pending logins for the user let logins = repo @@ -732,6 +734,7 @@ mod tests { .await .unwrap(); assert!(!logins.has_next_page); - assert_eq!(logins.edges, &[login]); + assert_eq!(logins.edges.len(), 1); + assert_eq!(logins.edges[0].node, login); } } diff --git a/crates/storage-pg/src/compat/session.rs b/crates/storage-pg/src/compat/session.rs index 4ba5ee726..0fb21c487 100644 --- a/crates/storage-pg/src/compat/session.rs +++ b/crates/storage-pg/src/compat/session.rs @@ -15,6 +15,7 @@ use mas_data_model::{ use mas_storage::{ Page, Pagination, compat::{CompatSessionFilter, CompatSessionRepository}, + pagination::Node, }; use rand::RngCore; use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def}; @@ -59,6 +60,12 @@ struct CompatSessionLookup { last_active_ip: Option, } +impl Node for CompatSessionLookup { + fn cursor(&self) -> Ulid { + self.compat_session_id.into() + } +} + impl From for CompatSession { fn from(value: CompatSessionLookup) -> Self { let id = value.compat_session_id.into(); @@ -106,6 +113,12 @@ struct CompatSessionAndSsoLoginLookup { compat_sso_login_exchanged_at: Option>, } +impl Node for CompatSessionAndSsoLoginLookup { + fn cursor(&self) -> Ulid { + self.compat_session_id.into() + } +} + impl TryFrom for (CompatSession, Option) { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/compat/sso_login.rs b/crates/storage-pg/src/compat/sso_login.rs index eeadff164..43ad4bead 100644 --- a/crates/storage-pg/src/compat/sso_login.rs +++ b/crates/storage-pg/src/compat/sso_login.rs @@ -10,6 +10,7 @@ use mas_data_model::{BrowserSession, Clock, CompatSession, CompatSsoLogin, Compa use mas_storage::{ Page, Pagination, compat::{CompatSsoLoginFilter, CompatSsoLoginRepository}, + pagination::Node, }; use rand::RngCore; use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def}; @@ -54,6 +55,12 @@ struct CompatSsoLoginLookup { compat_session_id: Option, } +impl Node for CompatSsoLoginLookup { + fn cursor(&self) -> Ulid { + self.compat_sso_login_id.into() + } +} + impl TryFrom for CompatSsoLogin { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index e6c03acc4..4e5a39139 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -39,6 +39,7 @@ pub enum Users { LockedAt, DeactivatedAt, CanRequestAdmin, + IsGuest, } #[derive(sea_query::Iden)] @@ -107,6 +108,35 @@ pub enum OAuth2Clients { IsStatic, } +#[derive(sea_query::Iden)] +#[iden = "personal_sessions"] +pub enum PersonalSessions { + Table, + PersonalSessionId, + OwnerUserId, + #[iden = "owner_oauth2_client_id"] + OwnerOAuth2ClientId, + ActorUserId, + HumanName, + ScopeList, + CreatedAt, + RevokedAt, + LastActiveAt, + LastActiveIp, +} + +#[derive(sea_query::Iden)] +#[iden = "personal_access_tokens"] +pub enum PersonalAccessTokens { + Table, + PersonalAccessTokenId, + PersonalSessionId, + // AccessTokenSha256, + CreatedAt, + ExpiresAt, + RevokedAt, +} + #[derive(sea_query::Iden)] #[iden = "upstream_oauth_providers"] pub enum UpstreamOAuthProviders { diff --git a/crates/storage-pg/src/lib.rs b/crates/storage-pg/src/lib.rs index 908058df6..207235667 100644 --- a/crates/storage-pg/src/lib.rs +++ b/crates/storage-pg/src/lib.rs @@ -165,6 +165,7 @@ use sqlx::migrate::Migrator; pub mod app_session; pub mod compat; pub mod oauth2; +pub mod personal; pub mod queue; pub mod upstream_oauth2; pub mod user; diff --git a/crates/storage-pg/src/oauth2/client.rs b/crates/storage-pg/src/oauth2/client.rs index ae7b03ac9..8f7d24224 100644 --- a/crates/storage-pg/src/oauth2/client.rs +++ b/crates/storage-pg/src/oauth2/client.rs @@ -811,6 +811,49 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { .await?; } + // Delete any personal access tokens & sessions owned + // by the client + { + let span = info_span!( + "db.oauth2_client.delete_by_id.personal_access_tokens", + { DB_QUERY_TEXT } = tracing::field::Empty, + ); + + sqlx::query!( + r#" + DELETE FROM personal_access_tokens + WHERE personal_session_id IN ( + SELECT personal_session_id + FROM personal_sessions + WHERE owner_oauth2_client_id = $1 + ) + "#, + Uuid::from(id), + ) + .record(&span) + .execute(&mut *self.conn) + .instrument(span) + .await?; + } + { + let span = info_span!( + "db.oauth2_client.delete_by_id.personal_sessions", + { DB_QUERY_TEXT } = tracing::field::Empty, + ); + + sqlx::query!( + r#" + DELETE FROM personal_sessions + WHERE owner_oauth2_client_id = $1 + "#, + Uuid::from(id), + ) + .record(&span) + .execute(&mut *self.conn) + .instrument(span) + .await?; + } + // Now delete the client itself let res = sqlx::query!( r#" diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index 6b1c81d46..bf741b5f2 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -511,10 +511,10 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 4); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); - assert_eq!(list.edges[2], session21); - assert_eq!(list.edges[3], session22); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); + assert_eq!(list.edges[2].node, session21); + assert_eq!(list.edges[3].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4); @@ -527,8 +527,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session21); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session21); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -541,8 +541,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -557,7 +557,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session22); + assert_eq!(list.edges[0].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -570,8 +570,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session12); - assert_eq!(list.edges[1], session21); + assert_eq!(list.edges[0].node, session12); + assert_eq!(list.edges[1].node, session21); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -584,8 +584,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session22); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); @@ -598,7 +598,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session22); + assert_eq!(list.edges[0].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -613,7 +613,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session22); + assert_eq!(list.edges[0].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -626,7 +626,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session12); + assert_eq!(list.edges[0].node, session12); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -641,7 +641,7 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session21); + assert_eq!(list.edges[0].node, session21); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); @@ -655,10 +655,10 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 4); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); - assert_eq!(list.edges[2], session21); - assert_eq!(list.edges[3], session22); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); + assert_eq!(list.edges[2].node, session21); + assert_eq!(list.edges[3].node, session22); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 4); // We should get all sessions with the "openid" and "email" scope @@ -671,8 +671,8 @@ mod tests { .unwrap(); assert!(!list.has_next_page); assert_eq!(list.edges.len(), 2); - assert_eq!(list.edges[0], session11); - assert_eq!(list.edges[1], session12); + assert_eq!(list.edges[0].node, session11); + assert_eq!(list.edges[1].node, session12); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 2); // Try combining the scope filter with the user filter @@ -685,7 +685,7 @@ mod tests { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0], session11); + assert_eq!(list.edges[0].node, session11); assert_eq!(repo.oauth2_session().count(filter).await.unwrap(), 1); // Finish all sessions of a client in batch diff --git a/crates/storage-pg/src/oauth2/session.rs b/crates/storage-pg/src/oauth2/session.rs index c04e03df0..072691a06 100644 --- a/crates/storage-pg/src/oauth2/session.rs +++ b/crates/storage-pg/src/oauth2/session.rs @@ -12,6 +12,7 @@ use mas_data_model::{BrowserSession, Client, Clock, Session, SessionState, User} use mas_storage::{ Page, Pagination, oauth2::{OAuth2SessionFilter, OAuth2SessionRepository}, + pagination::Node, }; use oauth2_types::scope::{Scope, ScopeToken}; use rand::RngCore; @@ -61,6 +62,12 @@ struct OAuthSessionLookup { human_name: Option, } +impl Node for OAuthSessionLookup { + fn cursor(&self) -> Ulid { + self.oauth2_session_id.into() + } +} + impl TryFrom for Session { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/personal/access_token.rs b/crates/storage-pg/src/personal/access_token.rs new file mode 100644 index 000000000..db8164fe9 --- /dev/null +++ b/crates/storage-pg/src/personal/access_token.rs @@ -0,0 +1,253 @@ +// 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. + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use mas_data_model::{ + Clock, + personal::{PersonalAccessToken, session::PersonalSession}, +}; +use mas_storage::personal::PersonalAccessTokenRepository; +use rand::RngCore; +use sha2::{Digest, Sha256}; +use sqlx::PgConnection; +use ulid::Ulid; +use uuid::Uuid; + +use crate::{DatabaseError, tracing::ExecuteExt as _}; + +/// An implementation of [`PersonalAccessTokenRepository`] for a PostgreSQL +/// connection +pub struct PgPersonalAccessTokenRepository<'c> { + conn: &'c mut PgConnection, +} + +impl<'c> PgPersonalAccessTokenRepository<'c> { + /// Create a new [`PgPersonalAccessTokenRepository`] from an active + /// PostgreSQL connection + pub fn new(conn: &'c mut PgConnection) -> Self { + Self { conn } + } +} + +struct PersonalAccessTokenLookup { + personal_access_token_id: Uuid, + personal_session_id: Uuid, + created_at: DateTime, + expires_at: Option>, + revoked_at: Option>, +} + +impl From for PersonalAccessToken { + fn from(value: PersonalAccessTokenLookup) -> Self { + Self { + id: Ulid::from(value.personal_access_token_id), + session_id: Ulid::from(value.personal_session_id), + created_at: value.created_at, + expires_at: value.expires_at, + revoked_at: value.revoked_at, + } + } +} + +#[async_trait] +impl PersonalAccessTokenRepository for PgPersonalAccessTokenRepository<'_> { + type Error = DatabaseError; + + #[tracing::instrument( + name = "db.personal_access_token.lookup", + skip_all, + fields( + db.query.text, + personal_access_token.id = %id, + ), + err, + )] + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error> { + let res = sqlx::query_as!( + PersonalAccessTokenLookup, + r#" + SELECT personal_access_token_id + , personal_session_id + , created_at + , expires_at + , revoked_at + + FROM personal_access_tokens + + WHERE personal_access_token_id = $1 + "#, + Uuid::from(id), + ) + .traced() + .fetch_optional(&mut *self.conn) + .await?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + name = "db.personal_access_token.find_by_token", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn find_by_token( + &mut self, + access_token: &str, + ) -> Result, Self::Error> { + let token_sha256 = Sha256::digest(access_token.as_bytes()).to_vec(); + + let res = sqlx::query_as!( + PersonalAccessTokenLookup, + r#" + SELECT personal_access_token_id + , personal_session_id + , created_at + , expires_at + , revoked_at + + FROM personal_access_tokens + + WHERE access_token_sha256 = $1 + "#, + &token_sha256, + ) + .traced() + .fetch_optional(&mut *self.conn) + .await?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + name = "db.personal_access_token.find_active_for_session", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn find_active_for_session( + &mut self, + session: &PersonalSession, + ) -> Result, Self::Error> { + let res: Option = sqlx::query_as!( + PersonalAccessTokenLookup, + r#" + SELECT personal_access_token_id + , personal_session_id + , created_at + , expires_at + , revoked_at + + FROM personal_access_tokens + + WHERE personal_session_id = $1 + AND revoked_at IS NULL + "#, + Uuid::from(session.id), + ) + .traced() + .fetch_optional(&mut *self.conn) + .await?; + + let Some(res) = res else { return Ok(None) }; + + Ok(Some(res.into())) + } + + #[tracing::instrument( + name = "db.personal_access_token.add", + skip_all, + fields( + db.query.text, + personal_access_token.id, + %session.id, + ), + err, + )] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &dyn Clock, + session: &PersonalSession, + access_token: &str, + expires_after: Option, + ) -> Result { + let created_at = clock.now(); + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + tracing::Span::current().record("personal_access_token.id", tracing::field::display(id)); + + let token_sha256 = Sha256::digest(access_token.as_bytes()).to_vec(); + + let expires_at = expires_after.map(|expires_after| created_at + expires_after); + + sqlx::query!( + r#" + INSERT INTO personal_access_tokens + (personal_access_token_id, personal_session_id, access_token_sha256, created_at, expires_at) + VALUES ($1, $2, $3, $4, $5) + "#, + Uuid::from(id), + Uuid::from(session.id), + &token_sha256, + created_at, + expires_at, + ) + .traced() + .execute(&mut *self.conn) + .await?; + + Ok(PersonalAccessToken { + id, + session_id: session.id, + created_at, + expires_at, + revoked_at: None, + }) + } + + #[tracing::instrument( + name = "db.personal_access_token.revoke", + skip_all, + fields( + db.query.text, + %access_token.id, + personal_session.id = %access_token.session_id, + ), + err, + )] + async fn revoke( + &mut self, + clock: &dyn Clock, + mut access_token: PersonalAccessToken, + ) -> Result { + let revoked_at = clock.now(); + let res = sqlx::query!( + r#" + UPDATE personal_access_tokens + SET revoked_at = $2 + WHERE personal_access_token_id = $1 + "#, + Uuid::from(access_token.id), + revoked_at, + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + access_token.revoked_at = Some(revoked_at); + Ok(access_token) + } +} diff --git a/crates/storage-pg/src/personal/mod.rs b/crates/storage-pg/src/personal/mod.rs new file mode 100644 index 000000000..f540a6be3 --- /dev/null +++ b/crates/storage-pg/src/personal/mod.rs @@ -0,0 +1,422 @@ +// 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. + +//! A module containing the PostgreSQL implementations of the +//! Personal Access Token / Personal Session repositories + +mod access_token; +mod session; + +pub use access_token::PgPersonalAccessTokenRepository; +pub use session::PgPersonalSessionRepository; + +#[cfg(test)] +mod tests { + use chrono::Duration; + use mas_data_model::{ + Clock, Device, clock::MockClock, personal::session::PersonalSessionOwner, + }; + use mas_storage::{ + Pagination, RepositoryAccess, + personal::{ + PersonalAccessTokenRepository, PersonalSessionFilter, PersonalSessionRepository, + }, + user::UserRepository, + }; + use oauth2_types::scope::{OPENID, PROFILE, Scope}; + use rand::SeedableRng; + use rand_chacha::ChaChaRng; + use sqlx::PgPool; + + use crate::PgRepository; + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_session_repository(pool: PgPool) { + let mut rng = ChaChaRng::seed_from_u64(42); + let clock = MockClock::default(); + let mut repo = PgRepository::from_pool(&pool).await.unwrap(); + + // Create a user + let admin_user = repo + .user() + .add(&mut rng, &clock, "john".to_owned()) + .await + .unwrap(); + let bot_user = repo + .user() + .add(&mut rng, &clock, "marvin".to_owned()) + .await + .unwrap(); + + let all = PersonalSessionFilter::new().for_actor_user(&bot_user); + let active = all.active_only(); + let finished = all.finished_only(); + let pagination = Pagination::first(10); + + assert_eq!(repo.personal_session().count(all).await.unwrap(), 0); + assert_eq!(repo.personal_session().count(active).await.unwrap(), 0); + assert_eq!(repo.personal_session().count(finished).await.unwrap(), 0); + + // We start off with no sessions + let full_list = repo.personal_session().list(all, pagination).await.unwrap(); + assert!(full_list.edges.is_empty()); + let active_list = repo + .personal_session() + .list(active, pagination) + .await + .unwrap(); + assert!(active_list.edges.is_empty()); + let finished_list = repo + .personal_session() + .list(finished, pagination) + .await + .unwrap(); + assert!(finished_list.edges.is_empty()); + + // Start a personal session for that user + let device = Device::generate(&mut rng); + let scope: Scope = [OPENID, PROFILE] + .into_iter() + .chain(device.to_scope_token().unwrap()) + .collect(); + let session = repo + .personal_session() + .add( + &mut rng, + &clock, + (&admin_user).into(), + &bot_user, + "Test Personal Session".to_owned(), + scope.clone(), + ) + .await + .unwrap(); + assert_eq!(session.owner, PersonalSessionOwner::User(admin_user.id)); + assert_eq!(session.actor_user_id, bot_user.id); + assert!(session.is_valid()); + assert!(!session.is_revoked()); + assert_eq!(session.scope, scope); + + assert_eq!(repo.personal_session().count(all).await.unwrap(), 1); + assert_eq!(repo.personal_session().count(active).await.unwrap(), 1); + assert_eq!(repo.personal_session().count(finished).await.unwrap(), 0); + + let full_list = repo.personal_session().list(all, pagination).await.unwrap(); + assert_eq!(full_list.edges.len(), 1); + assert_eq!(full_list.edges[0].node.0.id, session.id); + assert!(full_list.edges[0].node.0.is_valid()); + let active_list = repo + .personal_session() + .list(active, pagination) + .await + .unwrap(); + assert_eq!(active_list.edges.len(), 1); + assert_eq!(active_list.edges[0].node.0.id, session.id); + assert!(active_list.edges[0].node.0.is_valid()); + let finished_list = repo + .personal_session() + .list(finished, pagination) + .await + .unwrap(); + assert!(finished_list.edges.is_empty()); + + // Lookup the session and check it didn't change + let session_lookup = repo + .personal_session() + .lookup(session.id) + .await + .unwrap() + .expect("personal session not found"); + assert_eq!(session_lookup.id, session.id); + assert_eq!( + session_lookup.owner, + PersonalSessionOwner::User(admin_user.id) + ); + assert_eq!(session_lookup.actor_user_id, bot_user.id); + assert_eq!(session_lookup.scope, scope); + assert!(session_lookup.is_valid()); + assert!(!session_lookup.is_revoked()); + + // Revoke the session + let session = repo + .personal_session() + .revoke(&clock, session) + .await + .unwrap(); + assert!(!session.is_valid()); + assert!(session.is_revoked()); + + assert_eq!(repo.personal_session().count(all).await.unwrap(), 1); + assert_eq!(repo.personal_session().count(active).await.unwrap(), 0); + assert_eq!(repo.personal_session().count(finished).await.unwrap(), 1); + + let full_list = repo.personal_session().list(all, pagination).await.unwrap(); + assert_eq!(full_list.edges.len(), 1); + assert_eq!(full_list.edges[0].node.0.id, session.id); + let active_list = repo + .personal_session() + .list(active, pagination) + .await + .unwrap(); + assert!(active_list.edges.is_empty()); + let finished_list = repo + .personal_session() + .list(finished, pagination) + .await + .unwrap(); + assert_eq!(finished_list.edges.len(), 1); + assert_eq!(finished_list.edges[0].node.0.id, session.id); + assert!(finished_list.edges[0].node.0.is_revoked()); + + // Reload the session and check again + let session_lookup = repo + .personal_session() + .lookup(session.id) + .await + .unwrap() + .expect("personal session not found"); + assert!(!session_lookup.is_valid()); + assert!(session_lookup.is_revoked()); + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_session_revoke_bulk(pool: PgPool) { + let mut rng = ChaChaRng::seed_from_u64(42); + let clock = MockClock::default(); + let mut repo = PgRepository::from_pool(&pool).await.unwrap(); + + let alice_user = repo + .user() + .add(&mut rng, &clock, "alice".to_owned()) + .await + .unwrap(); + let bob_user = repo + .user() + .add(&mut rng, &clock, "bob".to_owned()) + .await + .unwrap(); + + let session1 = repo + .personal_session() + .add( + &mut rng, + &clock, + (&alice_user).into(), + &bob_user, + "Test Personal Session".to_owned(), + "openid".parse().unwrap(), + ) + .await + .unwrap(); + repo.personal_access_token() + .add( + &mut rng, + &clock, + &session1, + "mpt_hiss", + Some(Duration::days(42)), + ) + .await + .unwrap(); + + let session2 = repo + .personal_session() + .add( + &mut rng, + &clock, + (&bob_user).into(), + &bob_user, + "Test Personal Session".to_owned(), + "openid".parse().unwrap(), + ) + .await + .unwrap(); + repo.personal_access_token() + .add( + &mut rng, &clock, &session2, "mpt_meow", // No expiry + None, + ) + .await + .unwrap(); + + // Just one session without a token expiry time + assert_eq!( + repo.personal_session() + .revoke_bulk( + &clock, + PersonalSessionFilter::new() + .active_only() + .with_expires(false) + ) + .await + .unwrap(), + 1 + ); + + // Just one session with a token expiry time + assert_eq!( + repo.personal_session() + .revoke_bulk( + &clock, + PersonalSessionFilter::new() + .active_only() + .with_expires(true) + ) + .await + .unwrap(), + 1 + ); + + // No active sessions left + assert_eq!( + repo.personal_session() + .revoke_bulk(&clock, PersonalSessionFilter::new().active_only()) + .await + .unwrap(), + 0 + ); + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_access_token_repository(pool: PgPool) { + const FIRST_TOKEN: &str = "first_access_token"; + const SECOND_TOKEN: &str = "second_access_token"; + let mut rng = ChaChaRng::seed_from_u64(42); + let clock = MockClock::default(); + let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed(); + + // Create a user + let admin_user = repo + .user() + .add(&mut rng, &clock, "john".to_owned()) + .await + .unwrap(); + let bot_user = repo + .user() + .add(&mut rng, &clock, "marvin".to_owned()) + .await + .unwrap(); + + // Start a personal session for that user + let device = Device::generate(&mut rng); + let scope: Scope = [OPENID, PROFILE] + .into_iter() + .chain(device.to_scope_token().unwrap()) + .collect(); + let session = repo + .personal_session() + .add( + &mut rng, + &clock, + (&admin_user).into(), + &bot_user, + "Test Personal Session".to_owned(), + scope, + ) + .await + .unwrap(); + + // Add an access token to that session + let token = repo + .personal_access_token() + .add( + &mut rng, + &clock, + &session, + FIRST_TOKEN, + Some(Duration::try_minutes(1).unwrap()), + ) + .await + .unwrap(); + assert_eq!(token.session_id, session.id); + + // Commit the txn and grab a new transaction, to test a conflict + repo.save().await.unwrap(); + + { + let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed(); + // Adding the same token a second time should conflict + assert!( + repo.personal_access_token() + .add( + &mut rng, + &clock, + &session, + FIRST_TOKEN, + Some(Duration::try_minutes(1).unwrap()), + ) + .await + .is_err() + ); + repo.cancel().await.unwrap(); + } + + // Grab a new repo + let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed(); + + // Looking up via ID works + let token_lookup = repo + .personal_access_token() + .lookup(token.id) + .await + .unwrap() + .expect("personal access token not found"); + assert_eq!(token.id, token_lookup.id); + assert_eq!(token_lookup.session_id, session.id); + + // Looking up via the token value works + let token_lookup = repo + .personal_access_token() + .find_by_token(FIRST_TOKEN) + .await + .unwrap() + .expect("personal access token not found"); + assert_eq!(token.id, token_lookup.id); + assert_eq!(token_lookup.session_id, session.id); + + // Token is currently valid + assert!(token.is_valid(clock.now())); + + clock.advance(Duration::try_minutes(1).unwrap()); + // Token should have expired + assert!(!token.is_valid(clock.now())); + + // Add a second access token, this time without expiration + let _token = repo + .personal_access_token() + .revoke(&clock, token) + .await + .unwrap(); + let token = repo + .personal_access_token() + .add(&mut rng, &clock, &session, SECOND_TOKEN, None) + .await + .unwrap(); + assert_eq!(token.session_id, session.id); + + // Token is currently valid + assert!(token.is_valid(clock.now())); + + // Revoke it + let _token = repo + .personal_access_token() + .revoke(&clock, token) + .await + .unwrap(); + + // Reload it + let token = repo + .personal_access_token() + .find_by_token(SECOND_TOKEN) + .await + .unwrap() + .expect("personal access token not found"); + + // Token is not valid anymore + assert!(!token.is_valid(clock.now())); + + repo.save().await.unwrap(); + } +} diff --git a/crates/storage-pg/src/personal/session.rs b/crates/storage-pg/src/personal/session.rs new file mode 100644 index 000000000..b4c330ecb --- /dev/null +++ b/crates/storage-pg/src/personal/session.rs @@ -0,0 +1,702 @@ +// 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. + +use std::net::IpAddr; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use mas_data_model::{ + Clock, User, + personal::{ + PersonalAccessToken, + session::{PersonalSession, PersonalSessionOwner, SessionState}, + }, +}; +use mas_storage::{ + Page, Pagination, + pagination::Node, + personal::{PersonalSessionFilter, PersonalSessionRepository, PersonalSessionState}, +}; +use oauth2_types::scope::Scope; +use opentelemetry_semantic_conventions::trace::DB_QUERY_TEXT; +use rand::RngCore; +use sea_query::{ + Cond, Condition, Expr, PgFunc, PostgresQueryBuilder, Query, SimpleExpr, enum_def, + extension::postgres::PgExpr as _, +}; +use sea_query_binder::SqlxBinder as _; +use sqlx::PgConnection; +use tracing::{Instrument as _, info_span}; +use ulid::Ulid; +use uuid::Uuid; + +use crate::{ + DatabaseError, + errors::DatabaseInconsistencyError, + filter::{Filter, StatementExt as _}, + iden::{PersonalAccessTokens, PersonalSessions}, + pagination::QueryBuilderExt as _, + tracing::ExecuteExt as _, +}; + +/// An implementation of [`PersonalSessionRepository`] for a PostgreSQL +/// connection +pub struct PgPersonalSessionRepository<'c> { + conn: &'c mut PgConnection, +} + +impl<'c> PgPersonalSessionRepository<'c> { + /// Create a new [`PgPersonalSessionRepository`] from an active PostgreSQL + /// connection + pub fn new(conn: &'c mut PgConnection) -> Self { + Self { conn } + } +} + +#[derive(sqlx::FromRow)] +#[enum_def] +struct PersonalSessionLookup { + personal_session_id: Uuid, + owner_user_id: Option, + owner_oauth2_client_id: Option, + actor_user_id: Uuid, + human_name: String, + scope_list: Vec, + created_at: DateTime, + revoked_at: Option>, + last_active_at: Option>, + last_active_ip: Option, +} + +impl Node for PersonalSessionLookup { + fn cursor(&self) -> Ulid { + self.personal_session_id.into() + } +} + +impl TryFrom for PersonalSession { + type Error = DatabaseInconsistencyError; + + fn try_from(value: PersonalSessionLookup) -> Result { + let id = Ulid::from(value.personal_session_id); + let scope: Result = value.scope_list.iter().map(|s| s.parse()).collect(); + let scope = scope.map_err(|e| { + DatabaseInconsistencyError::on("personal_sessions") + .column("scope") + .row(id) + .source(e) + })?; + + let state = match value.revoked_at { + None => SessionState::Valid, + Some(revoked_at) => SessionState::Revoked { revoked_at }, + }; + + let owner = match (value.owner_user_id, value.owner_oauth2_client_id) { + (Some(owner_user_id), None) => PersonalSessionOwner::User(Ulid::from(owner_user_id)), + (None, Some(owner_oauth2_client_id)) => { + PersonalSessionOwner::OAuth2Client(Ulid::from(owner_oauth2_client_id)) + } + _ => { + // should be impossible (CHECK constraint in Postgres prevents it) + return Err(DatabaseInconsistencyError::on("personal_sessions") + .column("owner_user_id, owner_oauth2_client_id") + .row(id)); + } + }; + + Ok(PersonalSession { + id, + state, + owner, + actor_user_id: Ulid::from(value.actor_user_id), + human_name: value.human_name, + scope, + created_at: value.created_at, + last_active_at: value.last_active_at, + last_active_ip: value.last_active_ip, + }) + } +} + +#[derive(sqlx::FromRow)] +#[enum_def] +struct PersonalSessionAndAccessTokenLookup { + personal_session_id: Uuid, + owner_user_id: Option, + owner_oauth2_client_id: Option, + actor_user_id: Uuid, + human_name: String, + scope_list: Vec, + created_at: DateTime, + revoked_at: Option>, + last_active_at: Option>, + last_active_ip: Option, + + // tokens + personal_access_token_id: Option, + token_created_at: Option>, + token_expires_at: Option>, +} + +impl Node for PersonalSessionAndAccessTokenLookup { + fn cursor(&self) -> Ulid { + self.personal_session_id.into() + } +} + +impl TryFrom + for (PersonalSession, Option) +{ + type Error = DatabaseInconsistencyError; + + fn try_from(value: PersonalSessionAndAccessTokenLookup) -> Result { + let session = PersonalSession::try_from(PersonalSessionLookup { + personal_session_id: value.personal_session_id, + owner_user_id: value.owner_user_id, + owner_oauth2_client_id: value.owner_oauth2_client_id, + actor_user_id: value.actor_user_id, + human_name: value.human_name, + scope_list: value.scope_list, + created_at: value.created_at, + revoked_at: value.revoked_at, + last_active_at: value.last_active_at, + last_active_ip: value.last_active_ip, + })?; + + let token_opt = if let Some(id) = value.personal_access_token_id { + let id = Ulid::from(id); + Some(PersonalAccessToken { + id, + session_id: session.id, + // should not be possible + created_at: value.token_created_at.ok_or( + DatabaseInconsistencyError::on("personal_sessions") + .column("created_at") + .row(id), + )?, + expires_at: value.token_expires_at, + revoked_at: None, + }) + } else { + None + }; + + Ok((session, token_opt)) + } +} + +#[async_trait] +impl PersonalSessionRepository for PgPersonalSessionRepository<'_> { + type Error = DatabaseError; + + #[tracing::instrument( + name = "db.personal_session.lookup", + skip_all, + fields( + db.query.text, + session.id = %id, + ), + err, + )] + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error> { + let res = sqlx::query_as!( + PersonalSessionLookup, + r#" + SELECT personal_session_id + , owner_user_id + , owner_oauth2_client_id + , actor_user_id + , scope_list + , created_at + , revoked_at + , human_name + , last_active_at + , last_active_ip as "last_active_ip: IpAddr" + FROM personal_sessions + + WHERE personal_session_id = $1 + "#, + Uuid::from(id), + ) + .traced() + .fetch_optional(&mut *self.conn) + .await?; + + let Some(session) = res else { return Ok(None) }; + + Ok(Some(session.try_into()?)) + } + + #[tracing::instrument( + name = "db.personal_session.add", + skip_all, + fields( + db.query.text, + session.id, + session.scope = %scope, + ), + err, + )] + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &dyn Clock, + owner: PersonalSessionOwner, + actor_user: &User, + human_name: String, + scope: Scope, + ) -> Result { + let created_at = clock.now(); + let id = Ulid::from_datetime_with_source(created_at.into(), rng); + tracing::Span::current().record("session.id", tracing::field::display(id)); + + let scope_list: Vec = scope.iter().map(|s| s.as_str().to_owned()).collect(); + + let (owner_user_id, owner_oauth2_client_id) = match owner { + PersonalSessionOwner::User(ulid) => (Some(Uuid::from(ulid)), None), + PersonalSessionOwner::OAuth2Client(ulid) => (None, Some(Uuid::from(ulid))), + }; + + sqlx::query!( + r#" + INSERT INTO personal_sessions + ( personal_session_id + , owner_user_id + , owner_oauth2_client_id + , actor_user_id + , human_name + , scope_list + , created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + "#, + Uuid::from(id), + owner_user_id, + owner_oauth2_client_id, + Uuid::from(actor_user.id), + &human_name, + &scope_list, + created_at, + ) + .traced() + .execute(&mut *self.conn) + .await?; + + Ok(PersonalSession { + id, + state: SessionState::Valid, + owner, + actor_user_id: actor_user.id, + human_name, + scope, + created_at, + last_active_at: None, + last_active_ip: None, + }) + } + + #[tracing::instrument( + name = "db.personal_session.revoke", + skip_all, + fields( + db.query.text, + %session.id, + %session.scope, + ), + err, + )] + async fn revoke( + &mut self, + clock: &dyn Clock, + session: PersonalSession, + ) -> Result { + let revoked_at = clock.now(); + + { + // Revoke dependent PATs + let span = info_span!( + "db.personal_session.revoke.tokens", + { DB_QUERY_TEXT } = tracing::field::Empty, + ); + + sqlx::query!( + r#" + UPDATE personal_access_tokens + SET revoked_at = $2 + WHERE personal_session_id = $1 AND revoked_at IS NULL + "#, + Uuid::from(session.id), + revoked_at, + ) + .record(&span) + .execute(&mut *self.conn) + .instrument(span) + .await?; + } + + let res = sqlx::query!( + r#" + UPDATE personal_sessions + SET revoked_at = $2 + WHERE personal_session_id = $1 + "#, + Uuid::from(session.id), + revoked_at, + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + session + .finish(revoked_at) + .map_err(DatabaseError::to_invalid_operation) + } + + #[tracing::instrument( + name = "db.personal_session.revoke_bulk", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn revoke_bulk( + &mut self, + clock: &dyn Clock, + filter: PersonalSessionFilter<'_>, + ) -> Result { + let revoked_at = clock.now(); + + let (sql, arguments) = Query::update() + .table(PersonalSessions::Table) + .value(PersonalSessions::RevokedAt, revoked_at) + .and_where( + Expr::col((PersonalSessions::Table, PersonalSessions::PersonalSessionId)) + // Because filters apply to both the session and access token tables, + // Use a subquery to make it possible to use a JOIN + // onto the personal access token table. + .in_subquery( + Query::select() + .expr(Expr::col(( + PersonalSessions::Table, + PersonalSessions::PersonalSessionId, + ))) + .from(PersonalSessions::Table) + .left_join( + PersonalAccessTokens::Table, + Cond::all() + // Match session ID + .add( + Expr::col(( + PersonalSessions::Table, + PersonalSessions::PersonalSessionId, + )) + .eq(Expr::col(( + PersonalAccessTokens::Table, + PersonalAccessTokens::PersonalSessionId, + ))), + ) + // Only choose the active access token for each session + .add( + Expr::col(( + PersonalAccessTokens::Table, + PersonalAccessTokens::RevokedAt, + )) + .is_null(), + ), + ) + .apply_filter(filter) + .take(), + ), + ) + .build_sqlx(PostgresQueryBuilder); + + let res = sqlx::query_with(&sql, arguments) + .traced() + .execute(&mut *self.conn) + .await?; + + Ok(res.rows_affected().try_into().unwrap_or(usize::MAX)) + } + + #[tracing::instrument( + name = "db.personal_session.list", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn list( + &mut self, + filter: PersonalSessionFilter<'_>, + pagination: Pagination, + ) -> Result)>, Self::Error> { + let (sql, arguments) = Query::select() + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::PersonalSessionId)), + PersonalSessionAndAccessTokenLookupIden::PersonalSessionId, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::OwnerUserId)), + PersonalSessionAndAccessTokenLookupIden::OwnerUserId, + ) + .expr_as( + Expr::col(( + PersonalSessions::Table, + PersonalSessions::OwnerOAuth2ClientId, + )), + PersonalSessionAndAccessTokenLookupIden::OwnerOauth2ClientId, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::ActorUserId)), + PersonalSessionAndAccessTokenLookupIden::ActorUserId, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::HumanName)), + PersonalSessionAndAccessTokenLookupIden::HumanName, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::ScopeList)), + PersonalSessionAndAccessTokenLookupIden::ScopeList, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::CreatedAt)), + PersonalSessionAndAccessTokenLookupIden::CreatedAt, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::RevokedAt)), + PersonalSessionAndAccessTokenLookupIden::RevokedAt, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveAt)), + PersonalSessionAndAccessTokenLookupIden::LastActiveAt, + ) + .expr_as( + Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveIp)), + PersonalSessionAndAccessTokenLookupIden::LastActiveIp, + ) + .expr_as( + Expr::col(( + PersonalAccessTokens::Table, + PersonalAccessTokens::PersonalAccessTokenId, + )), + PersonalSessionAndAccessTokenLookupIden::PersonalAccessTokenId, + ) + .expr_as( + Expr::col((PersonalAccessTokens::Table, PersonalAccessTokens::CreatedAt)), + PersonalSessionAndAccessTokenLookupIden::TokenCreatedAt, + ) + .expr_as( + Expr::col((PersonalAccessTokens::Table, PersonalAccessTokens::ExpiresAt)), + PersonalSessionAndAccessTokenLookupIden::TokenExpiresAt, + ) + .from(PersonalSessions::Table) + .left_join( + PersonalAccessTokens::Table, + Cond::all() + // Match session ID + .add( + Expr::col((PersonalSessions::Table, PersonalSessions::PersonalSessionId)) + .eq(Expr::col(( + PersonalAccessTokens::Table, + PersonalAccessTokens::PersonalSessionId, + ))), + ) + // Only choose the active access token for each session + .add( + Expr::col((PersonalAccessTokens::Table, PersonalAccessTokens::RevokedAt)) + .is_null(), + ), + ) + .apply_filter(filter) + .generate_pagination( + (PersonalSessions::Table, PersonalSessions::PersonalSessionId), + pagination, + ) + .build_sqlx(PostgresQueryBuilder); + + let edges: Vec = sqlx::query_as_with(&sql, arguments) + .traced() + .fetch_all(&mut *self.conn) + .await?; + + let page = pagination.process(edges).try_map(TryFrom::try_from)?; + + Ok(page) + } + + #[tracing::instrument( + name = "db.personal_session.count", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result { + let (sql, arguments) = Query::select() + .expr(Expr::col((PersonalSessions::Table, PersonalSessions::PersonalSessionId)).count()) + .from(PersonalSessions::Table) + .left_join( + PersonalAccessTokens::Table, + Cond::all() + // Match session ID + .add( + Expr::col((PersonalSessions::Table, PersonalSessions::PersonalSessionId)) + .eq(Expr::col(( + PersonalAccessTokens::Table, + PersonalAccessTokens::PersonalSessionId, + ))), + ) + // Only choose the active access token for each session + .add( + Expr::col((PersonalAccessTokens::Table, PersonalAccessTokens::RevokedAt)) + .is_null(), + ), + ) + .apply_filter(filter) + .build_sqlx(PostgresQueryBuilder); + + let count: i64 = sqlx::query_scalar_with(&sql, arguments) + .traced() + .fetch_one(&mut *self.conn) + .await?; + + count + .try_into() + .map_err(DatabaseError::to_invalid_operation) + } + + #[tracing::instrument( + name = "db.personal_session.record_batch_activity", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn record_batch_activity( + &mut self, + mut activities: Vec<(Ulid, DateTime, Option)>, + ) -> Result<(), Self::Error> { + // Sort the activity by ID, so that when batching the updates, Postgres + // locks the rows in a stable order, preventing deadlocks + activities.sort_unstable(); + let mut ids = Vec::with_capacity(activities.len()); + let mut last_activities = Vec::with_capacity(activities.len()); + let mut ips = Vec::with_capacity(activities.len()); + + for (id, last_activity, ip) in activities { + ids.push(Uuid::from(id)); + last_activities.push(last_activity); + ips.push(ip); + } + + let res = sqlx::query!( + r#" + UPDATE personal_sessions + SET last_active_at = GREATEST(t.last_active_at, personal_sessions.last_active_at) + , last_active_ip = COALESCE(t.last_active_ip, personal_sessions.last_active_ip) + FROM ( + SELECT * + FROM UNNEST($1::uuid[], $2::timestamptz[], $3::inet[]) + AS t(personal_session_id, last_active_at, last_active_ip) + ) AS t + WHERE personal_sessions.personal_session_id = t.personal_session_id + "#, + &ids, + &last_activities, + &ips as &[Option], + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, ids.len().try_into().unwrap_or(u64::MAX))?; + + Ok(()) + } +} + +impl Filter for PersonalSessionFilter<'_> { + fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition { + sea_query::Condition::all() + .add_option(self.owner_user().map(|user| { + Expr::col((PersonalSessions::Table, PersonalSessions::OwnerUserId)) + .eq(Uuid::from(user.id)) + })) + .add_option(self.owner_oauth2_client().map(|client| { + Expr::col(( + PersonalSessions::Table, + PersonalSessions::OwnerOAuth2ClientId, + )) + .eq(Uuid::from(client.id)) + })) + .add_option(self.actor_user().map(|user| { + Expr::col((PersonalSessions::Table, PersonalSessions::ActorUserId)) + .eq(Uuid::from(user.id)) + })) + .add_option(self.device().map(|device| -> SimpleExpr { + if let Ok([stable_scope_token, unstable_scope_token]) = device.to_scope_token() { + Condition::any() + .add( + Expr::val(stable_scope_token.to_string()).eq(PgFunc::any(Expr::col(( + PersonalSessions::Table, + PersonalSessions::ScopeList, + )))), + ) + .add(Expr::val(unstable_scope_token.to_string()).eq(PgFunc::any( + Expr::col((PersonalSessions::Table, PersonalSessions::ScopeList)), + ))) + .into() + } else { + // If the device ID can't be encoded as a scope token, match no rows + Expr::val(false).into() + } + })) + .add_option(self.state().map(|state| match state { + PersonalSessionState::Active => { + Expr::col((PersonalSessions::Table, PersonalSessions::RevokedAt)).is_null() + } + PersonalSessionState::Revoked => { + Expr::col((PersonalSessions::Table, PersonalSessions::RevokedAt)).is_not_null() + } + })) + .add_option(self.scope().map(|scope| { + let scope: Vec = scope.iter().map(|s| s.as_str().to_owned()).collect(); + Expr::col((PersonalSessions::Table, PersonalSessions::ScopeList)).contains(scope) + })) + .add_option(self.last_active_before().map(|last_active_before| { + Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveAt)) + .lt(last_active_before) + })) + .add_option(self.last_active_after().map(|last_active_after| { + Expr::col((PersonalSessions::Table, PersonalSessions::LastActiveAt)) + .gt(last_active_after) + })) + .add_option(self.expires_before().map(|expires_before| { + Expr::col((PersonalAccessTokens::Table, PersonalAccessTokens::ExpiresAt)) + .lt(expires_before) + })) + .add_option(self.expires_after().map(|expires_after| { + Expr::col((PersonalAccessTokens::Table, PersonalAccessTokens::ExpiresAt)) + .gt(expires_after) + })) + .add_option(self.expires().map(|expires| { + let column = + Expr::col((PersonalAccessTokens::Table, PersonalAccessTokens::ExpiresAt)); + + if expires { + column.is_not_null() + } else { + column.is_null() + } + })) + } +} diff --git a/crates/storage-pg/src/repository.rs b/crates/storage-pg/src/repository.rs index 7911cd2b6..210d66a02 100644 --- a/crates/storage-pg/src/repository.rs +++ b/crates/storage-pg/src/repository.rs @@ -20,6 +20,7 @@ use mas_storage::{ OAuth2AccessTokenRepository, OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2DeviceCodeGrantRepository, OAuth2RefreshTokenRepository, OAuth2SessionRepository, }, + personal::PersonalSessionRepository, policy_data::PolicyDataRepository, queue::{QueueJobRepository, QueueScheduleRepository, QueueWorkerRepository}, upstream_oauth2::{ @@ -47,6 +48,7 @@ use crate::{ PgOAuth2ClientRepository, PgOAuth2DeviceCodeGrantRepository, PgOAuth2RefreshTokenRepository, PgOAuth2SessionRepository, }, + personal::{PgPersonalAccessTokenRepository, PgPersonalSessionRepository}, policy_data::PgPolicyDataRepository, queue::{ job::PgQueueJobRepository, schedule::PgQueueScheduleRepository, @@ -328,6 +330,19 @@ where Box::new(PgCompatRefreshTokenRepository::new(self.conn.as_mut())) } + fn personal_access_token<'c>( + &'c mut self, + ) -> Box + 'c> + { + Box::new(PgPersonalAccessTokenRepository::new(self.conn.as_mut())) + } + + fn personal_session<'c>( + &'c mut self, + ) -> Box + 'c> { + Box::new(PgPersonalSessionRepository::new(self.conn.as_mut())) + } + fn queue_worker<'c>(&'c mut self) -> Box + 'c> { Box::new(PgQueueWorkerRepository::new(self.conn.as_mut())) } diff --git a/crates/storage-pg/src/upstream_oauth2/link.rs b/crates/storage-pg/src/upstream_oauth2/link.rs index 6f671655d..c43dd8a18 100644 --- a/crates/storage-pg/src/upstream_oauth2/link.rs +++ b/crates/storage-pg/src/upstream_oauth2/link.rs @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc}; use mas_data_model::{Clock, UpstreamOAuthLink, UpstreamOAuthProvider, User}; use mas_storage::{ Page, Pagination, + pagination::Node, upstream_oauth2::{UpstreamOAuthLinkFilter, UpstreamOAuthLinkRepository}, }; use opentelemetry_semantic_conventions::trace::DB_QUERY_TEXT; @@ -53,6 +54,12 @@ struct LinkLookup { created_at: DateTime, } +impl Node for LinkLookup { + fn cursor(&self) -> Ulid { + self.upstream_oauth_link_id.into() + } +} + impl From for UpstreamOAuthLink { fn from(value: LinkLookup) -> Self { UpstreamOAuthLink { diff --git a/crates/storage-pg/src/upstream_oauth2/mod.rs b/crates/storage-pg/src/upstream_oauth2/mod.rs index 6381c9b7f..d98e840b6 100644 --- a/crates/storage-pg/src/upstream_oauth2/mod.rs +++ b/crates/storage-pg/src/upstream_oauth2/mod.rs @@ -206,8 +206,8 @@ mod tests { assert!(!links.has_previous_page); assert!(!links.has_next_page); assert_eq!(links.edges.len(), 1); - assert_eq!(links.edges[0].id, link.id); - assert_eq!(links.edges[0].user_id, Some(user.id)); + assert_eq!(links.edges[0].node.id, link.id); + assert_eq!(links.edges[0].node.user_id, Some(user.id)); assert_eq!(repo.upstream_oauth_link().count(filter).await.unwrap(), 1); @@ -282,7 +282,7 @@ mod tests { .unwrap(); assert_eq!(session_page.edges.len(), 1); - assert_eq!(session_page.edges[0].id, session.id); + assert_eq!(session_page.edges[0].node.id, session.id); assert!(!session_page.has_next_page); assert!(!session_page.has_previous_page); @@ -374,7 +374,7 @@ mod tests { // It returned the first 10 items assert!(page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Getting the same page with the "enabled only" filter should return the same @@ -396,7 +396,7 @@ mod tests { // It returned the next 10 items assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the last 10 items @@ -408,7 +408,7 @@ mod tests { // It returned the last 10 items assert!(page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the previous 10 items @@ -420,7 +420,7 @@ mod tests { // It returned the previous 10 items assert!(!page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Lookup 10 items between two IDs @@ -432,7 +432,7 @@ mod tests { // It returned the items in between assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|p| p.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|p| p.node.id).collect(); assert_eq!(&edge_ids, &ids[6..8]); // There should not be any disabled providers @@ -560,7 +560,7 @@ mod tests { // It returned the first 10 items assert!(page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Lookup the next 10 items @@ -572,7 +572,7 @@ mod tests { // It returned the next 10 items assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the last 10 items @@ -584,7 +584,7 @@ mod tests { // It returned the last 10 items assert!(page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[10..]); // Lookup the previous 10 items @@ -596,7 +596,7 @@ mod tests { // It returned the previous 10 items assert!(!page.has_previous_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[..10]); // Lookup 5 items between two IDs @@ -608,7 +608,7 @@ mod tests { // It returned the items in between assert!(!page.has_next_page); - let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.node.id).collect(); assert_eq!(&edge_ids, &ids[6..11]); // Check the sub/sid filters @@ -638,11 +638,21 @@ mod tests { assert_eq!(page.edges.len(), 4); for edge in page.edges { assert_eq!( - edge.id_token_claims().unwrap().get("sub").unwrap().as_str(), + edge.node + .id_token_claims() + .unwrap() + .get("sub") + .unwrap() + .as_str(), Some("alice") ); assert_eq!( - edge.id_token_claims().unwrap().get("sid").unwrap().as_str(), + edge.node + .id_token_claims() + .unwrap() + .get("sid") + .unwrap() + .as_str(), Some("one") ); } diff --git a/crates/storage-pg/src/upstream_oauth2/provider.rs b/crates/storage-pg/src/upstream_oauth2/provider.rs index 583f8ec0c..caade738d 100644 --- a/crates/storage-pg/src/upstream_oauth2/provider.rs +++ b/crates/storage-pg/src/upstream_oauth2/provider.rs @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc}; use mas_data_model::{Clock, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports}; use mas_storage::{ Page, Pagination, + pagination::Node, upstream_oauth2::{ UpstreamOAuthProviderFilter, UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository, }, @@ -74,6 +75,12 @@ struct ProviderLookup { on_backchannel_logout: String, } +impl Node for ProviderLookup { + fn cursor(&self) -> Ulid { + self.upstream_oauth_provider_id.into() + } +} + impl TryFrom for UpstreamOAuthProvider { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/upstream_oauth2/session.rs b/crates/storage-pg/src/upstream_oauth2/session.rs index 8b37aae2e..b961c4f8c 100644 --- a/crates/storage-pg/src/upstream_oauth2/session.rs +++ b/crates/storage-pg/src/upstream_oauth2/session.rs @@ -12,6 +12,7 @@ use mas_data_model::{ }; use mas_storage::{ Page, Pagination, + pagination::Node, upstream_oauth2::{UpstreamOAuthSessionFilter, UpstreamOAuthSessionRepository}, }; use rand::RngCore; @@ -91,6 +92,12 @@ struct SessionLookup { unlinked_at: Option>, } +impl Node for SessionLookup { + fn cursor(&self) -> Ulid { + self.upstream_oauth_authorization_session_id.into() + } +} + impl TryFrom for UpstreamOAuthAuthorizationSession { type Error = DatabaseInconsistencyError; diff --git a/crates/storage-pg/src/user/email.rs b/crates/storage-pg/src/user/email.rs index 916874ae8..0f998e55f 100644 --- a/crates/storage-pg/src/user/email.rs +++ b/crates/storage-pg/src/user/email.rs @@ -12,6 +12,7 @@ use mas_data_model::{ }; use mas_storage::{ Page, Pagination, + pagination::Node, user::{UserEmailFilter, UserEmailRepository}, }; use rand::RngCore; @@ -51,6 +52,12 @@ struct UserEmailLookup { created_at: DateTime, } +impl Node for UserEmailLookup { + fn cursor(&self) -> Ulid { + self.user_email_id.into() + } +} + impl From for UserEmail { fn from(e: UserEmailLookup) -> UserEmail { UserEmail { diff --git a/crates/storage-pg/src/user/mod.rs b/crates/storage-pg/src/user/mod.rs index 8af61b61c..bbf02567e 100644 --- a/crates/storage-pg/src/user/mod.rs +++ b/crates/storage-pg/src/user/mod.rs @@ -11,7 +11,7 @@ use async_trait::async_trait; use mas_data_model::{Clock, User}; use mas_storage::user::{UserFilter, UserRepository}; use rand::RngCore; -use sea_query::{Expr, PostgresQueryBuilder, Query}; +use sea_query::{Expr, PostgresQueryBuilder, Query, extension::postgres::PgExpr as _}; use sea_query_binder::SqlxBinder; use sqlx::PgConnection; use ulid::Ulid; @@ -61,7 +61,9 @@ mod priv_ { #![allow(missing_docs)] use chrono::{DateTime, Utc}; + use mas_storage::pagination::Node; use sea_query::enum_def; + use ulid::Ulid; use uuid::Uuid; #[derive(Debug, Clone, sqlx::FromRow)] @@ -73,6 +75,13 @@ mod priv_ { pub(super) locked_at: Option>, pub(super) deactivated_at: Option>, pub(super) can_request_admin: bool, + pub(super) is_guest: bool, + } + + impl Node for UserLookup { + fn cursor(&self) -> Ulid { + self.user_id.into() + } } } @@ -89,6 +98,7 @@ impl From for User { locked_at: value.locked_at, deactivated_at: value.deactivated_at, can_request_admin: value.can_request_admin, + is_guest: value.is_guest, } } } @@ -114,6 +124,13 @@ impl Filter for UserFilter<'_> { .add_option(self.can_request_admin().map(|can_request_admin| { Expr::col((Users::Table, Users::CanRequestAdmin)).eq(can_request_admin) })) + .add_option( + self.is_guest() + .map(|is_guest| Expr::col((Users::Table, Users::IsGuest)).eq(is_guest)), + ) + .add_option(self.search().map(|search| { + Expr::col((Users::Table, Users::Username)).ilike(format!("%{search}%")) + })) } } @@ -140,6 +157,7 @@ impl UserRepository for PgUserRepository<'_> { , locked_at , deactivated_at , can_request_admin + , is_guest FROM users WHERE user_id = $1 "#, @@ -176,6 +194,7 @@ impl UserRepository for PgUserRepository<'_> { , locked_at , deactivated_at , can_request_admin + , is_guest FROM users WHERE LOWER(username) = LOWER($1) "#, @@ -249,6 +268,7 @@ impl UserRepository for PgUserRepository<'_> { locked_at: None, deactivated_at: None, can_request_admin: false, + is_guest: false, }) } @@ -487,6 +507,10 @@ impl UserRepository for PgUserRepository<'_> { Expr::col((Users::Table, Users::CanRequestAdmin)), UserLookupIden::CanRequestAdmin, ) + .expr_as( + Expr::col((Users::Table, Users::IsGuest)), + UserLookupIden::IsGuest, + ) .from(Users::Table) .apply_filter(filter) .generate_pagination((Users::Table, Users::UserId), pagination) diff --git a/crates/storage-pg/src/user/registration_token.rs b/crates/storage-pg/src/user/registration_token.rs index f64c8136d..5c9231aa1 100644 --- a/crates/storage-pg/src/user/registration_token.rs +++ b/crates/storage-pg/src/user/registration_token.rs @@ -8,6 +8,7 @@ use chrono::{DateTime, Utc}; use mas_data_model::{Clock, UserRegistrationToken}; use mas_storage::{ Page, Pagination, + pagination::Node, user::{UserRegistrationTokenFilter, UserRegistrationTokenRepository}, }; use rand::RngCore; @@ -53,6 +54,12 @@ struct UserRegistrationTokenLookup { revoked_at: Option>, } +impl Node for UserRegistrationTokenLookup { + fn cursor(&self) -> Ulid { + self.user_registration_token_id.into() + } +} + impl Filter for UserRegistrationTokenFilter { fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition { sea_query::Condition::all() @@ -230,7 +237,7 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { filter: UserRegistrationTokenFilter, pagination: Pagination, ) -> Result, Self::Error> { - let (sql, values) = Query::select() + let (sql, arguments) = Query::select() .expr_as( Expr::col(( UserRegistrationTokens::Table, @@ -295,15 +302,14 @@ impl UserRegistrationTokenRepository for PgUserRegistrationTokenRepository<'_> { ) .build_sqlx(PostgresQueryBuilder); - let tokens = sqlx::query_as_with::<_, UserRegistrationTokenLookup, _>(&sql, values) + let edges: Vec = sqlx::query_as_with(&sql, arguments) .traced() .fetch_all(&mut *self.conn) - .await? - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()?; + .await?; - let page = pagination.process(tokens); + let page = pagination + .process(edges) + .try_map(UserRegistrationToken::try_from)?; Ok(page) } @@ -705,7 +711,7 @@ mod tests { .await .unwrap(); - assert!(page.edges.iter().any(|t| t.id == unrevoked_token.id)); + assert!(page.edges.iter().any(|t| t.node.id == unrevoked_token.id)); } #[sqlx::test(migrator = "crate::MIGRATOR")] @@ -867,7 +873,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token2.id); + assert_eq!(page.edges[0].node.id, token2.id); // Test unused filter let unused_filter = UserRegistrationTokenFilter::new(clock.now()).with_been_used(false); @@ -886,7 +892,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token3.id); + assert_eq!(page.edges[0].node.id, token3.id); let not_expired_filter = UserRegistrationTokenFilter::new(clock.now()).with_expired(false); let page = repo @@ -904,7 +910,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token4.id); + assert_eq!(page.edges[0].node.id, token4.id); let not_revoked_filter = UserRegistrationTokenFilter::new(clock.now()).with_revoked(false); let page = repo @@ -941,7 +947,7 @@ mod tests { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, token4.id); + assert_eq!(page.edges[0].node.id, token4.id); // Test pagination let page = repo diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index a8c5ab458..8644d7666 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -14,6 +14,7 @@ use mas_data_model::{ }; use mas_storage::{ Page, Pagination, + pagination::Node, user::{BrowserSessionFilter, BrowserSessionRepository}, }; use rand::RngCore; @@ -61,6 +62,13 @@ struct SessionLookup { user_locked_at: Option>, user_deactivated_at: Option>, user_can_request_admin: bool, + user_is_guest: bool, +} + +impl Node for SessionLookup { + fn cursor(&self) -> Ulid { + self.user_id.into() + } } impl TryFrom for BrowserSession { @@ -76,6 +84,7 @@ impl TryFrom for BrowserSession { locked_at: value.user_locked_at, deactivated_at: value.user_deactivated_at, can_request_admin: value.user_can_request_admin, + is_guest: value.user_is_guest, }; Ok(BrowserSession { @@ -201,6 +210,7 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> { , u.locked_at AS "user_locked_at" , u.deactivated_at AS "user_deactivated_at" , u.can_request_admin AS "user_can_request_admin" + , u.is_guest AS "user_is_guest" FROM user_sessions s INNER JOIN users u USING (user_id) @@ -391,6 +401,10 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> { Expr::col((Users::Table, Users::CanRequestAdmin)), SessionLookupIden::UserCanRequestAdmin, ) + .expr_as( + Expr::col((Users::Table, Users::IsGuest)), + SessionLookupIden::UserIsGuest, + ) .from(UserSessions::Table) .inner_join( Users::Table, diff --git a/crates/storage-pg/src/user/tests.rs b/crates/storage-pg/src/user/tests.rs index 0880c1032..98489d68d 100644 --- a/crates/storage-pg/src/user/tests.rs +++ b/crates/storage-pg/src/user/tests.rs @@ -174,10 +174,23 @@ async fn test_user_repo(pool: PgPool) { assert_eq!(repo.user().count(locked).await.unwrap(), 0); assert_eq!(repo.user().count(deactivated).await.unwrap(), 1); + // Test the search filter + assert_eq!( + repo.user() + .count(all.matching_search("alice")) + .await + .unwrap(), + 0 + ); + assert_eq!( + repo.user().count(all.matching_search("JO")).await.unwrap(), + 1 + ); + // Check the list method let list = repo.user().list(all, Pagination::first(10)).await.unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].id, user.id); + assert_eq!(list.edges[0].node.id, user.id); let list = repo .user() @@ -192,7 +205,7 @@ async fn test_user_repo(pool: PgPool) { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].id, user.id); + assert_eq!(list.edges[0].node.id, user.id); let list = repo .user() @@ -214,7 +227,7 @@ async fn test_user_repo(pool: PgPool) { .await .unwrap(); assert_eq!(list.edges.len(), 1); - assert_eq!(list.edges[0].id, user.id); + assert_eq!(list.edges[0].node.id, user.id); repo.save().await.unwrap(); } @@ -335,7 +348,7 @@ async fn test_user_email_repo(pool: PgPool) { .unwrap(); assert!(!emails.has_next_page); assert_eq!(emails.edges.len(), 1); - assert_eq!(emails.edges[0], user_email); + assert_eq!(emails.edges[0].node, user_email); // Listing emails from the email address should work let emails = repo @@ -345,7 +358,7 @@ async fn test_user_email_repo(pool: PgPool) { .unwrap(); assert!(!emails.has_next_page); assert_eq!(emails.edges.len(), 1); - assert_eq!(emails.edges[0], user_email); + assert_eq!(emails.edges[0].node, user_email); // Filtering on another email should not return anything let emails = repo @@ -635,7 +648,7 @@ async fn test_user_session(pool: PgPool) { .unwrap(); assert!(!session_list.has_next_page); assert_eq!(session_list.edges.len(), 1); - assert_eq!(session_list.edges[0], session); + assert_eq!(session_list.edges[0].node, session); let session_lookup = repo .browser_session() @@ -796,7 +809,7 @@ async fn test_user_session(pool: PgPool) { .await .unwrap(); assert_eq!(page.edges.len(), 1); - assert_eq!(page.edges[0].id, session.id); + assert_eq!(page.edges[0].node.id, session.id); // Try counting assert_eq!(repo.browser_session().count(filter).await.unwrap(), 1); diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 605dea279..c5d5d0b26 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -20,7 +20,7 @@ //! To define a new repository, you have to: //! 1. Define a new (async) repository trait, with the methods you need //! 2. Write an implementation of this trait for each storage backend you want -//! (currently only for [`mas-storage-pg`]) +//! (currently only for `mas-storage-pg`) //! 3. Make it accessible via the [`RepositoryAccess`] trait //! //! The repository trait definition should look like this: @@ -111,6 +111,7 @@ mod utils; pub mod app_session; pub mod compat; pub mod oauth2; +pub mod personal; pub mod policy_data; pub mod queue; pub mod upstream_oauth2; diff --git a/crates/storage/src/pagination.rs b/crates/storage/src/pagination.rs index 01b8ed197..ad632cb10 100644 --- a/crates/storage/src/pagination.rs +++ b/crates/storage/src/pagination.rs @@ -16,12 +16,12 @@ pub struct InvalidPagination; /// Pagination parameters #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Pagination { +pub struct Pagination { /// The cursor to start from - pub before: Option, + pub before: Option, /// The cursor to end at - pub after: Option, + pub after: Option, /// The maximum number of items to return pub count: usize, @@ -40,16 +40,22 @@ pub enum PaginationDirection { Backward, } -impl Pagination { +/// A node in a page, with a cursor +pub trait Node { + /// The cursor of that particular node + fn cursor(&self) -> C; +} + +impl Pagination { /// Creates a new [`Pagination`] from user-provided parameters. /// /// # Errors /// /// Either `first` or `last` must be provided, else this function will /// return an [`InvalidPagination`] error. - pub const fn try_new( - before: Option, - after: Option, + pub fn try_new( + before: Option, + after: Option, first: Option, last: Option, ) -> Result { @@ -91,49 +97,57 @@ impl Pagination { /// Get items before the given cursor #[must_use] - pub const fn before(mut self, id: Ulid) -> Self { - self.before = Some(id); + pub fn before(mut self, cursor: C) -> Self { + self.before = Some(cursor); self } /// Clear the before cursor #[must_use] - pub const fn clear_before(mut self) -> Self { + pub fn clear_before(mut self) -> Self { self.before = None; self } /// Get items after the given cursor #[must_use] - pub const fn after(mut self, id: Ulid) -> Self { - self.after = Some(id); + pub fn after(mut self, cursor: C) -> Self { + self.after = Some(cursor); self } /// Clear the after cursor #[must_use] - pub const fn clear_after(mut self) -> Self { + pub fn clear_after(mut self) -> Self { self.after = None; self } /// Process a page returned by a paginated query #[must_use] - pub fn process(&self, mut edges: Vec) -> Page { - let is_full = edges.len() == (self.count + 1); + pub fn process>(&self, mut nodes: Vec) -> Page { + let is_full = nodes.len() == (self.count + 1); if is_full { - edges.pop(); + nodes.pop(); } let (has_previous_page, has_next_page) = match self.direction { PaginationDirection::Forward => (false, is_full), PaginationDirection::Backward => { // 6. If the last argument is provided, I reverse the order of the results - edges.reverse(); + nodes.reverse(); (is_full, false) } }; + let edges = nodes + .into_iter() + .map(|node| Edge { + cursor: node.cursor(), + node, + }) + .collect(); + Page { has_next_page, has_previous_page, @@ -142,9 +156,18 @@ impl Pagination { } } +/// An edge in a paginated result +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Edge { + /// The cursor of the edge + pub cursor: C, + /// The node of the edge + pub node: T, +} + /// A page of results returned by a paginated query #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Page { +pub struct Page { /// When paginating forwards, this is true if there are more items after pub has_next_page: bool, @@ -152,21 +175,28 @@ pub struct Page { pub has_previous_page: bool, /// The items in the page - pub edges: Vec, + pub edges: Vec>, } -impl Page { +impl Page { /// Map the items in this page with the given function /// /// # Parameters /// /// * `f`: The function to map the items with #[must_use] - pub fn map(self, f: F) -> Page + pub fn map(self, mut f: F) -> Page where F: FnMut(T) -> T2, { - let edges = self.edges.into_iter().map(f).collect(); + let edges = self + .edges + .into_iter() + .map(|edge| Edge { + cursor: edge.cursor, + node: f(edge.node), + }) + .collect(); Page { has_next_page: self.has_next_page, has_previous_page: self.has_previous_page, @@ -183,11 +213,21 @@ impl Page { /// # Errors /// /// Returns the first error encountered while mapping the items - pub fn try_map(self, f: F) -> Result, E> + pub fn try_map(self, mut f: F) -> Result, E> where F: FnMut(T) -> Result, { - let edges: Result, E> = self.edges.into_iter().map(f).collect(); + let edges: Result>, E> = self + .edges + .into_iter() + .map(|edge| { + Ok(Edge { + cursor: edge.cursor, + node: f(edge.node)?, + }) + }) + .collect(); + Ok(Page { has_next_page: self.has_next_page, has_previous_page: self.has_previous_page, diff --git a/crates/storage/src/personal/access_token.rs b/crates/storage/src/personal/access_token.rs new file mode 100644 index 000000000..363a3199f --- /dev/null +++ b/crates/storage/src/personal/access_token.rs @@ -0,0 +1,140 @@ +// 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. + +use async_trait::async_trait; +use chrono::Duration; +use mas_data_model::{ + Clock, + personal::{PersonalAccessToken, session::PersonalSession}, +}; +use rand_core::RngCore; +use ulid::Ulid; + +use crate::repository_impl; + +/// An [`PersonalAccessTokenRepository`] helps interacting with +/// [`PersonalAccessToken`] saved in the storage backend +#[async_trait] +pub trait PersonalAccessTokenRepository: Send + Sync { + /// The error type returned by the repository + type Error; + + /// Lookup an access token by its ID + /// + /// Returns the access token if it exists, `None` otherwise + /// + /// # Parameters + /// + /// * `id`: The ID of the access token to lookup + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; + + /// Find an access token by its token + /// + /// Returns the access token if it exists, `None` otherwise + /// + /// # Parameters + /// + /// * `access_token`: The token of the access token to lookup + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn find_by_token( + &mut self, + access_token: &str, + ) -> Result, Self::Error>; + + /// Find the active access token belonging to a given session. + /// + /// Returns the active access token if it exists, `None` otherwise + /// + /// # Parameters + /// + /// * `session`: The session to lookup + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn find_active_for_session( + &mut self, + session: &PersonalSession, + ) -> Result, Self::Error>; + + /// Add a new access token to the database + /// + /// Returns the newly created access token + /// + /// # Parameters + /// + /// * `rng`: A random number generator + /// * `clock`: The clock used to generate timestamps + /// * `session`: The session the access token is associated with + /// * `access_token`: The access token to add + /// * `expires_after`: The duration after which the access token expires. If + /// [`None`] the access token never expires + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &dyn Clock, + session: &PersonalSession, + access_token: &str, + expires_after: Option, + ) -> Result; + + /// Revoke an access token + /// + /// Returns the revoked access token + /// + /// # Parameters + /// + /// * `clock`: The clock used to generate timestamps + /// * `access_token`: The access token to revoke + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn revoke( + &mut self, + clock: &dyn Clock, + access_token: PersonalAccessToken, + ) -> Result; +} + +repository_impl!(PersonalAccessTokenRepository: + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; + + async fn find_by_token( + &mut self, + access_token: &str, + ) -> Result, Self::Error>; + + async fn find_active_for_session( + &mut self, + session: &PersonalSession, + ) -> Result, Self::Error>; + + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &dyn Clock, + session: &PersonalSession, + access_token: &str, + expires_after: Option, + ) -> Result; + + async fn revoke( + &mut self, + clock: &dyn Clock, + access_token: PersonalAccessToken, + ) -> Result; +); diff --git a/crates/storage/src/personal/mod.rs b/crates/storage/src/personal/mod.rs new file mode 100644 index 000000000..3a9dfcd65 --- /dev/null +++ b/crates/storage/src/personal/mod.rs @@ -0,0 +1,16 @@ +// 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. + +//! Repositories to deal with Personal Sessions and Personal Access Tokens +//! (PATs), which are sessions/access tokens created manually by users for use +//! in scripts, bots and similar applications. + +mod access_token; +mod session; + +pub use self::{ + access_token::PersonalAccessTokenRepository, + session::{PersonalSessionFilter, PersonalSessionRepository, PersonalSessionState}, +}; diff --git a/crates/storage/src/personal/session.rs b/crates/storage/src/personal/session.rs new file mode 100644 index 000000000..921c6df39 --- /dev/null +++ b/crates/storage/src/personal/session.rs @@ -0,0 +1,398 @@ +// 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. + +use std::net::IpAddr; + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use mas_data_model::{ + Client, Clock, Device, User, + personal::{ + PersonalAccessToken, + session::{PersonalSession, PersonalSessionOwner}, + }, +}; +use oauth2_types::scope::Scope; +use rand_core::RngCore; +use ulid::Ulid; + +use crate::{Page, Pagination, repository_impl}; + +/// A [`PersonalSessionRepository`] helps interacting with +/// [`PersonalSession`] saved in the storage backend +#[async_trait] +pub trait PersonalSessionRepository: Send + Sync { + /// The error type returned by the repository + type Error; + + /// Lookup a Personal session by its ID + /// + /// Returns the Personal session if it exists, `None` otherwise + /// + /// # Parameters + /// + /// * `id`: The ID of the Personal session to lookup + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; + + /// Start a new Personal session + /// + /// Returns the newly created Personal session + /// + /// # Parameters + /// + /// * `rng`: The random number generator to use + /// * `clock`: The clock used to generate timestamps + /// * `owner_user`: The user that will own the personal session + /// * `actor_user`: The user that will be represented by the personal + /// session + /// * `device`: The device ID of this session + /// * `human_name`: The human-readable name of the session provided by the + /// client or the user + /// * `scope`: The [`Scope`] of the [`PersonalSession`] + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &dyn Clock, + owner: PersonalSessionOwner, + actor_user: &User, + human_name: String, + scope: Scope, + ) -> Result; + + /// End a Personal session + /// + /// Returns the ended Personal session + /// + /// # Parameters + /// + /// * `clock`: The clock used to generate timestamps + /// * `Personal_session`: The Personal session to end + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn revoke( + &mut self, + clock: &dyn Clock, + personal_session: PersonalSession, + ) -> Result; + + /// Revoke all the [`PersonalSession`]s matching the given filter. + /// + /// Returns the number of sessions affected + /// + /// # Parameters + /// + /// * `clock`: The clock used to generate timestamps + /// * `filter`: The filter to apply + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn revoke_bulk( + &mut self, + clock: &dyn Clock, + filter: PersonalSessionFilter<'_>, + ) -> Result; + + /// List [`PersonalSession`]s matching the given filter and pagination + /// parameters + /// + /// # Parameters + /// + /// * `filter`: The filter parameters + /// * `pagination`: The pagination parameters + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn list( + &mut self, + filter: PersonalSessionFilter<'_>, + pagination: Pagination, + ) -> Result)>, Self::Error>; + + /// Count [`PersonalSession`]s matching the given filter + /// + /// # Parameters + /// + /// * `filter`: The filter parameters + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result; + + /// Record a batch of [`PersonalSession`] activity + /// + /// # Parameters + /// + /// * `activity`: A list of tuples containing the session ID, the last + /// activity timestamp and the IP address of the client + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn record_batch_activity( + &mut self, + activity: Vec<(Ulid, DateTime, Option)>, + ) -> Result<(), Self::Error>; +} + +repository_impl!(PersonalSessionRepository: + async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>; + + async fn add( + &mut self, + rng: &mut (dyn RngCore + Send), + clock: &dyn Clock, + owner: PersonalSessionOwner, + actor_user: &User, + human_name: String, + scope: Scope, + ) -> Result; + + async fn revoke( + &mut self, + clock: &dyn Clock, + personal_session: PersonalSession, + ) -> Result; + + async fn revoke_bulk( + &mut self, + clock: &dyn Clock, + filter: PersonalSessionFilter<'_>, + ) -> Result; + + async fn list( + &mut self, + filter: PersonalSessionFilter<'_>, + pagination: Pagination, + ) -> Result)>, Self::Error>; + + async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result; + + async fn record_batch_activity( + &mut self, + activity: Vec<(Ulid, DateTime, Option)>, + ) -> Result<(), Self::Error>; +); + +/// Filter parameters for listing personal sessions alongside personal access +/// tokens +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct PersonalSessionFilter<'a> { + owner_user: Option<&'a User>, + owner_oauth2_client: Option<&'a Client>, + actor_user: Option<&'a User>, + device: Option<&'a Device>, + state: Option, + scope: Option<&'a Scope>, + last_active_before: Option>, + last_active_after: Option>, + expires_before: Option>, + expires_after: Option>, + expires: Option, +} + +/// Filter for what state a personal session is in. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PersonalSessionState { + /// The personal session is active, which means it either + /// has active access tokens or can have new access tokens generated. + Active, + /// The personal session is revoked, which means no more access tokens + /// can be generated and none are active. + Revoked, +} + +impl<'a> PersonalSessionFilter<'a> { + /// Create a new [`PersonalSessionFilter`] with default values + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// List sessions owned by a specific user + #[must_use] + pub fn for_owner_user(mut self, user: &'a User) -> Self { + self.owner_user = Some(user); + self + } + + /// Get the owner user filter + /// + /// Returns [`None`] if no user filter was set + #[must_use] + pub fn owner_oauth2_client(&self) -> Option<&'a Client> { + self.owner_oauth2_client + } + + /// List sessions owned by a specific user + #[must_use] + pub fn for_owner_oauth2_client(mut self, client: &'a Client) -> Self { + self.owner_oauth2_client = Some(client); + self + } + + /// Get the owner user filter + /// + /// Returns [`None`] if no user filter was set + #[must_use] + pub fn owner_user(&self) -> Option<&'a User> { + self.owner_user + } + + /// List sessions acting as a specific user + #[must_use] + pub fn for_actor_user(mut self, user: &'a User) -> Self { + self.actor_user = Some(user); + self + } + + /// Get the actor user filter + /// + /// Returns [`None`] if no user filter was set + #[must_use] + pub fn actor_user(&self) -> Option<&'a User> { + self.actor_user + } + + /// Only return sessions with a last active time before the given time + #[must_use] + pub fn with_last_active_before(mut self, last_active_before: DateTime) -> Self { + self.last_active_before = Some(last_active_before); + self + } + + /// Only return sessions with a last active time after the given time + #[must_use] + pub fn with_last_active_after(mut self, last_active_after: DateTime) -> Self { + self.last_active_after = Some(last_active_after); + self + } + + /// Get the last active before filter + /// + /// Returns [`None`] if no client filter was set + #[must_use] + pub fn last_active_before(&self) -> Option> { + self.last_active_before + } + + /// Get the last active after filter + /// + /// Returns [`None`] if no client filter was set + #[must_use] + pub fn last_active_after(&self) -> Option> { + self.last_active_after + } + + /// Only return active sessions + #[must_use] + pub fn active_only(mut self) -> Self { + self.state = Some(PersonalSessionState::Active); + self + } + + /// Only return finished sessions + #[must_use] + pub fn finished_only(mut self) -> Self { + self.state = Some(PersonalSessionState::Revoked); + self + } + + /// Get the state filter + /// + /// Returns [`None`] if no state filter was set + #[must_use] + pub fn state(&self) -> Option { + self.state + } + + /// Only return sessions with the given scope + #[must_use] + pub fn with_scope(mut self, scope: &'a Scope) -> Self { + self.scope = Some(scope); + self + } + + /// Get the scope filter + /// + /// Returns [`None`] if no scope filter was set + #[must_use] + pub fn scope(&self) -> Option<&'a Scope> { + self.scope + } + + /// Only return sessions that have the given device in their scope + #[must_use] + pub fn for_device(mut self, device: &'a Device) -> Self { + self.device = Some(device); + self + } + + /// Get the device filter + /// + /// Returns [`None`] if no device filter was set + #[must_use] + pub fn device(&self) -> Option<&'a Device> { + self.device + } + + /// Only return sessions whose access tokens expire before the given time + #[must_use] + pub fn with_expires_before(mut self, expires_before: DateTime) -> Self { + self.expires_before = Some(expires_before); + self + } + + /// Get the expires before filter + /// + /// Returns [`None`] if no expires before filter was set + #[must_use] + pub fn expires_before(&self) -> Option> { + self.expires_before + } + + /// Only return sessions whose access tokens expire after the given time + #[must_use] + pub fn with_expires_after(mut self, expires_after: DateTime) -> Self { + self.expires_after = Some(expires_after); + self + } + + /// Get the expires after filter + /// + /// Returns [`None`] if no expires after filter was set + #[must_use] + pub fn expires_after(&self) -> Option> { + self.expires_after + } + + /// Only return sessions whose access tokens have, or don't have, + /// an expiry time set + #[must_use] + pub fn with_expires(mut self, expires: bool) -> Self { + self.expires = Some(expires); + self + } + + /// Get the expires filter + /// + /// Returns [`None`] if no expires filter was set + #[must_use] + pub fn expires(&self) -> Option { + self.expires + } +} diff --git a/crates/storage/src/queue/tasks.rs b/crates/storage/src/queue/tasks.rs index eb16f6e29..3558314cf 100644 --- a/crates/storage/src/queue/tasks.rs +++ b/crates/storage/src/queue/tasks.rs @@ -384,7 +384,7 @@ impl ExpireInactiveOAuthSessionsJob { let last_edge = page.edges.last()?; Some(Self { threshold: self.threshold, - after: Some(last_edge.id), + after: Some(last_edge.cursor), }) } } @@ -441,7 +441,7 @@ impl ExpireInactiveCompatSessionsJob { let last_edge = page.edges.last()?; Some(Self { threshold: self.threshold, - after: Some(last_edge.id), + after: Some(last_edge.cursor), }) } } @@ -498,7 +498,7 @@ impl ExpireInactiveUserSessionsJob { let last_edge = page.edges.last()?; Some(Self { threshold: self.threshold, - after: Some(last_edge.id), + after: Some(last_edge.cursor), }) } } diff --git a/crates/storage/src/repository.rs b/crates/storage/src/repository.rs index 518769eb1..f6eb191e6 100644 --- a/crates/storage/src/repository.rs +++ b/crates/storage/src/repository.rs @@ -18,6 +18,7 @@ use crate::{ OAuth2AccessTokenRepository, OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2DeviceCodeGrantRepository, OAuth2RefreshTokenRepository, OAuth2SessionRepository, }, + personal::{PersonalAccessTokenRepository, PersonalSessionRepository}, policy_data::PolicyDataRepository, queue::{QueueJobRepository, QueueScheduleRepository, QueueWorkerRepository}, upstream_oauth2::{ @@ -214,6 +215,16 @@ pub trait RepositoryAccess: Send { &'c mut self, ) -> Box + 'c>; + /// Get a [`PersonalAccessTokenRepository`] + fn personal_access_token<'c>( + &'c mut self, + ) -> Box + 'c>; + + /// Get a [`PersonalSessionRepository`] + fn personal_session<'c>( + &'c mut self, + ) -> Box + 'c>; + /// Get a [`QueueWorkerRepository`] fn queue_worker<'c>(&'c mut self) -> Box + 'c>; @@ -247,6 +258,7 @@ mod impls { OAuth2ClientRepository, OAuth2DeviceCodeGrantRepository, OAuth2RefreshTokenRepository, OAuth2SessionRepository, }, + personal::{PersonalAccessTokenRepository, PersonalSessionRepository}, policy_data::PolicyDataRepository, queue::{QueueJobRepository, QueueScheduleRepository, QueueWorkerRepository}, upstream_oauth2::{ @@ -458,6 +470,21 @@ mod impls { )) } + fn personal_access_token<'c>( + &'c mut self, + ) -> Box + 'c> { + Box::new(MapErr::new( + self.inner.personal_access_token(), + &mut self.mapper, + )) + } + + fn personal_session<'c>( + &'c mut self, + ) -> Box + 'c> { + Box::new(MapErr::new(self.inner.personal_session(), &mut self.mapper)) + } + fn queue_worker<'c>( &'c mut self, ) -> Box + 'c> { @@ -610,6 +637,18 @@ mod impls { (**self).compat_refresh_token() } + fn personal_access_token<'c>( + &'c mut self, + ) -> Box + 'c> { + (**self).personal_access_token() + } + + fn personal_session<'c>( + &'c mut self, + ) -> Box + 'c> { + (**self).personal_session() + } + fn queue_worker<'c>( &'c mut self, ) -> Box + 'c> { diff --git a/crates/storage/src/user/mod.rs b/crates/storage/src/user/mod.rs index 0294d83b4..657909ef4 100644 --- a/crates/storage/src/user/mod.rs +++ b/crates/storage/src/user/mod.rs @@ -75,10 +75,11 @@ impl UserState { pub struct UserFilter<'a> { state: Option, can_request_admin: Option, - _phantom: std::marker::PhantomData<&'a ()>, + is_guest: Option, + search: Option<&'a str>, } -impl UserFilter<'_> { +impl<'a> UserFilter<'a> { /// Create a new [`UserFilter`] with default values #[must_use] pub fn new() -> Self { @@ -120,6 +121,27 @@ impl UserFilter<'_> { self } + /// Filter for guest users + #[must_use] + pub fn guest_only(mut self) -> Self { + self.is_guest = Some(true); + self + } + + /// Filter for non-guest users + #[must_use] + pub fn non_guest_only(mut self) -> Self { + self.is_guest = Some(false); + self + } + + /// Filter for users that match the given search string + #[must_use] + pub fn matching_search(mut self, search: &'a str) -> Self { + self.search = Some(search); + self + } + /// Get the state filter /// /// Returns [`None`] if no state filter was set @@ -135,6 +157,22 @@ impl UserFilter<'_> { pub fn can_request_admin(&self) -> Option { self.can_request_admin } + + /// Get the is guest filter + /// + /// Returns [`None`] if no is guest filter was set + #[must_use] + pub fn is_guest(&self) -> Option { + self.is_guest + } + + /// Get the search filter + /// + /// Returns [`None`] if no search filter was set + #[must_use] + pub fn search(&self) -> Option<&'a str> { + self.search + } } /// A [`UserRepository`] helps interacting with [`User`] saved in the storage diff --git a/crates/syn2mas/src/synapse_reader/config/oidc.rs b/crates/syn2mas/src/synapse_reader/config/oidc.rs index 49ee59a71..09baba165 100644 --- a/crates/syn2mas/src/synapse_reader/config/oidc.rs +++ b/crates/syn2mas/src/synapse_reader/config/oidc.rs @@ -7,9 +7,9 @@ use std::{collections::BTreeMap, str::FromStr as _}; use chrono::{DateTime, Utc}; use mas_config::{ - UpstreamOAuth2ClaimsImports, UpstreamOAuth2DiscoveryMode, UpstreamOAuth2ImportAction, - UpstreamOAuth2OnBackchannelLogout, UpstreamOAuth2PkceMethod, UpstreamOAuth2ResponseMode, - UpstreamOAuth2TokenAuthMethod, + ClientSecret, UpstreamOAuth2ClaimsImports, UpstreamOAuth2DiscoveryMode, + UpstreamOAuth2ImportAction, UpstreamOAuth2OnBackchannelLogout, UpstreamOAuth2PkceMethod, + UpstreamOAuth2ResponseMode, UpstreamOAuth2TokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use oauth2_types::scope::{OPENID, Scope, ScopeToken}; @@ -328,7 +328,7 @@ impl OidcProvider { human_name: self.idp_name, brand_name: self.idp_brand, client_id, - client_secret: self.client_secret, + client_secret: self.client_secret.map(ClientSecret::Value), token_endpoint_auth_method, sign_in_with_apple: None, token_endpoint_auth_signing_alg: None, diff --git a/crates/tasks/src/matrix.rs b/crates/tasks/src/matrix.rs index 87e052d20..68905fe53 100644 --- a/crates/tasks/src/matrix.rs +++ b/crates/tasks/src/matrix.rs @@ -14,6 +14,7 @@ use mas_storage::{ Pagination, RepositoryAccess, compat::CompatSessionFilter, oauth2::OAuth2SessionFilter, + personal::PersonalSessionFilter, queue::{ DeleteDeviceJob, ProvisionDeviceJob, ProvisionUserJob, QueueJobRepositoryExt as _, SyncDevicesJob, @@ -203,11 +204,12 @@ impl RunnableJob for SyncDevicesJob { .await .map_err(JobError::retry)?; - for (compat_session, _) in page.edges { + for edge in page.edges { + let (compat_session, _) = edge.node; if let Some(ref device) = compat_session.device { devices.insert(device.as_str().to_owned()); } - cursor = cursor.after(compat_session.id); + cursor = cursor.after(edge.cursor); } if !page.has_next_page { @@ -227,14 +229,44 @@ impl RunnableJob for SyncDevicesJob { .await .map_err(JobError::retry)?; - for oauth2_session in page.edges { - for scope in &*oauth2_session.scope { + for edge in page.edges { + for scope in &*edge.node.scope { if let Some(device) = Device::from_scope_token(scope) { devices.insert(device.as_str().to_owned()); } } - cursor = cursor.after(oauth2_session.id); + cursor = cursor.after(edge.cursor); + } + + if !page.has_next_page { + break; + } + } + + // Cycle through all the personal sessions of the user and get the devices + let mut cursor = Pagination::first(5000); + loop { + let page = repo + .personal_session() + .list( + PersonalSessionFilter::new() + .for_actor_user(&user) + .active_only(), + cursor, + ) + .await + .map_err(JobError::retry)?; + + for edge in page.edges { + let (session, _) = &edge.node; + for scope in &*session.scope { + if let Some(device) = Device::from_scope_token(scope) { + devices.insert(device.as_str().to_owned()); + } + } + + cursor = cursor.after(edge.cursor); } if !page.has_next_page { diff --git a/crates/tasks/src/recovery.rs b/crates/tasks/src/recovery.rs index 03e02d57b..51afcc295 100644 --- a/crates/tasks/src/recovery.rs +++ b/crates/tasks/src/recovery.rs @@ -70,26 +70,18 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { .await .map_err(JobError::retry)?; - for email in page.edges { + for edge in page.edges { let ticket = Alphanumeric.sample_string(&mut rng, 32); let ticket = repo .user_recovery() - .add_ticket(&mut rng, clock, &session, &email, ticket) + .add_ticket(&mut rng, clock, &session, &edge.node, ticket) .await .map_err(JobError::retry)?; - let user_email = repo - .user_email() - .lookup(email.id) - .await - .map_err(JobError::retry)? - .context("User email not found") - .map_err(JobError::fail)?; - let user = repo .user() - .lookup(user_email.user_id) + .lookup(edge.node.user_id) .await .map_err(JobError::retry)? .context("User not found") @@ -97,7 +89,7 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { let url = url_builder.account_recovery_link(ticket.ticket); - let address: Address = user_email.email.parse().map_err(JobError::fail)?; + let address: Address = edge.node.email.parse().map_err(JobError::fail)?; let mailbox = Mailbox::new(Some(user.username.clone()), address); info!("Sending recovery email to {}", mailbox); @@ -112,7 +104,7 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { ); } - cursor = cursor.after(email.id); + cursor = cursor.after(edge.cursor); } if !page.has_next_page { diff --git a/crates/tasks/src/sessions.rs b/crates/tasks/src/sessions.rs index d10d908da..eede69d51 100644 --- a/crates/tasks/src/sessions.rs +++ b/crates/tasks/src/sessions.rs @@ -110,7 +110,7 @@ impl RunnableJob for ExpireInactiveOAuthSessionsJob { } for edge in page.edges { - if let Some(user_id) = edge.user_id { + if let Some(user_id) = edge.node.user_id { let inserted = users_synced.insert(user_id); if inserted { tracing::info!(user.id = %user_id, "Scheduling devices sync for user"); @@ -128,7 +128,7 @@ impl RunnableJob for ExpireInactiveOAuthSessionsJob { } repo.oauth2_session() - .finish(clock, edge) + .finish(clock, edge.node) .await .map_err(JobError::retry)?; } @@ -174,14 +174,14 @@ impl RunnableJob for ExpireInactiveCompatSessionsJob { } for edge in page.edges { - let inserted = users_synced.insert(edge.user_id); + let inserted = users_synced.insert(edge.node.user_id); if inserted { - tracing::info!(user.id = %edge.user_id, "Scheduling devices sync for user"); + tracing::info!(user.id = %edge.node.user_id, "Scheduling devices sync for user"); repo.queue_job() .schedule_job_later( &mut rng, clock, - SyncDevicesJob::new_for_id(edge.user_id), + SyncDevicesJob::new_for_id(edge.node.user_id), clock.now() + delay, ) .await @@ -190,7 +190,7 @@ impl RunnableJob for ExpireInactiveCompatSessionsJob { } repo.compat_session() - .finish(clock, edge) + .finish(clock, edge.node) .await .map_err(JobError::retry)?; } @@ -230,7 +230,7 @@ impl RunnableJob for ExpireInactiveUserSessionsJob { for edge in page.edges { repo.browser_session() - .finish(clock, edge) + .finish(clock, edge.node) .await .map_err(JobError::retry)?; } diff --git a/crates/tasks/src/user.rs b/crates/tasks/src/user.rs index ee60c6532..e605670cc 100644 --- a/crates/tasks/src/user.rs +++ b/crates/tasks/src/user.rs @@ -10,6 +10,7 @@ use mas_storage::{ RepositoryAccess, compat::CompatSessionFilter, oauth2::OAuth2SessionFilter, + personal::PersonalSessionFilter, queue::{DeactivateUserJob, ReactivateUserJob}, user::{BrowserSessionFilter, UserEmailFilter, UserRepository}, }; @@ -80,6 +81,36 @@ impl RunnableJob for DeactivateUserJob { .map_err(JobError::retry)?; info!(affected = n, "Killed all compatibility sessions for user"); + let n = repo + .personal_session() + .revoke_bulk( + clock, + PersonalSessionFilter::new() + .for_actor_user(&user) + .active_only(), + ) + .await + .map_err(JobError::retry)?; + info!( + affected = n, + "Killed all compatibility sessions acting as user" + ); + + let n = repo + .personal_session() + .revoke_bulk( + clock, + PersonalSessionFilter::new() + .for_owner_user(&user) + .active_only(), + ) + .await + .map_err(JobError::retry)?; + info!( + affected = n, + "Killed all compatibility sessions owned by user" + ); + // Delete all the email addresses for the user let n = repo .user_email() diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 0a04677eb..597683a03 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -12,6 +12,7 @@ mod ext; mod features; use std::{ + collections::BTreeMap, fmt::Formatter, net::{IpAddr, Ipv4Addr}, }; @@ -105,21 +106,53 @@ pub trait TemplateContext: Serialize { /// /// This is then used to check for template validity in unit tests and in /// the CLI (`cargo run -- templates check`) - fn sample(now: chrono::DateTime, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + locales: &[DataLocale], + ) -> BTreeMap where Self: Sized; } +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SampleIdentifier { + pub components: Vec<(&'static str, String)>, +} + +impl SampleIdentifier { + pub fn from_index(index: usize) -> Self { + Self { + components: Vec::default(), + } + .with_appended("index", format!("{index}")) + } + + pub fn with_appended(&self, kind: &'static str, locale: String) -> Self { + let mut new = self.clone(); + new.components.push((kind, locale)); + new + } +} + +pub(crate) fn sample_list(samples: Vec) -> BTreeMap { + samples + .into_iter() + .enumerate() + .map(|(index, sample)| (SampleIdentifier::from_index(index), sample)) + .collect() +} + impl TemplateContext for () { fn sample( _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - Vec::new() + BTreeMap::new() } } @@ -148,7 +181,11 @@ impl std::ops::Deref for WithLanguage { } impl TemplateContext for WithLanguage { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { @@ -157,9 +194,14 @@ impl TemplateContext for WithLanguage { .flat_map(|locale| { T::sample(now, rng, locales) .into_iter() - .map(move |inner| WithLanguage { - lang: locale.to_string(), - inner, + .map(|(sample_id, sample)| { + ( + sample_id.with_appended("locale", locale.to_string()), + WithLanguage { + lang: locale.to_string(), + inner: sample, + }, + ) }) }) .collect() @@ -176,15 +218,24 @@ pub struct WithCsrf { } impl TemplateContext for WithCsrf { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { T::sample(now, rng, locales) .into_iter() - .map(|inner| WithCsrf { - csrf_token: "fake_csrf_token".into(), - inner, + .map(|(k, inner)| { + ( + k, + WithCsrf { + csrf_token: "fake_csrf_token".into(), + inner, + }, + ) }) .collect() } @@ -200,18 +251,28 @@ pub struct WithSession { } impl TemplateContext for WithSession { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { BrowserSession::samples(now, rng) .into_iter() - .flat_map(|session| { + .enumerate() + .flat_map(|(session_index, session)| { T::sample(now, rng, locales) .into_iter() - .map(move |inner| WithSession { - current_session: session.clone(), - inner, + .map(move |(k, inner)| { + ( + k.with_appended("browser-session", session_index.to_string()), + WithSession { + current_session: session.clone(), + inner, + }, + ) }) }) .collect() @@ -228,7 +289,11 @@ pub struct WithOptionalSession { } impl TemplateContext for WithOptionalSession { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { @@ -236,12 +301,22 @@ impl TemplateContext for WithOptionalSession { .into_iter() .map(Some) // Wrap all samples in an Option .chain(std::iter::once(None)) // Add the "None" option - .flat_map(|session| { + .enumerate() + .flat_map(|(session_index, session)| { T::sample(now, rng, locales) .into_iter() - .map(move |inner| WithOptionalSession { - current_session: session.clone(), - inner, + .map(move |(k, inner)| { + ( + if session.is_some() { + k.with_appended("browser-session", session_index.to_string()) + } else { + k + }, + WithOptionalSession { + current_session: session.clone(), + inner, + }, + ) }) }) .collect() @@ -269,11 +344,11 @@ impl TemplateContext for EmptyContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![EmptyContext] + sample_list(vec![EmptyContext]) } } @@ -297,15 +372,15 @@ impl TemplateContext for IndexContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![Self { + sample_list(vec![Self { discovery_url: "https://example.com/.well-known/openid-configuration" .parse() .unwrap(), - }] + }]) } } @@ -343,12 +418,12 @@ impl TemplateContext for AppContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None); - vec![Self::from_url_builder(&url_builder)] + sample_list(vec![Self::from_url_builder(&url_builder)]) } } @@ -376,12 +451,12 @@ impl TemplateContext for ApiDocContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None); - vec![Self::from_url_builder(&url_builder)] + sample_list(vec![Self::from_url_builder(&url_builder)]) } } @@ -468,12 +543,12 @@ impl TemplateContext for LoginContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { // TODO: samples with errors - vec![ + sample_list(vec![ LoginContext { form: FormState::default(), next: None, @@ -503,7 +578,7 @@ impl TemplateContext for LoginContext { next: None, providers: Vec::new(), }, - ] + ]) } } @@ -576,14 +651,14 @@ impl TemplateContext for RegisterContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![RegisterContext { + sample_list(vec![RegisterContext { providers: Vec::new(), next: None, - }] + }]) } } @@ -619,15 +694,15 @@ impl TemplateContext for PasswordRegisterContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { // TODO: samples with errors - vec![PasswordRegisterContext { + sample_list(vec![PasswordRegisterContext { form: FormState::default(), next: None, - }] + }]) } } @@ -657,24 +732,30 @@ pub struct ConsentContext { } impl TemplateContext for ConsentContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - Client::samples(now, rng) - .into_iter() - .map(|client| { - let mut grant = AuthorizationGrant::sample(now, rng); - let action = PostAuthAction::continue_grant(grant.id); - // XXX - grant.client_id = client.id; - Self { - grant, - client, - action, - } - }) - .collect() + sample_list( + Client::samples(now, rng) + .into_iter() + .map(|client| { + let mut grant = AuthorizationGrant::sample(now, rng); + let action = PostAuthAction::continue_grant(grant.id); + // XXX + grant.client_id = client.id; + Self { + grant, + client, + action, + } + }) + .collect(), + ) } } @@ -709,38 +790,44 @@ pub struct PolicyViolationContext { } impl TemplateContext for PolicyViolationContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - Client::samples(now, rng) - .into_iter() - .flat_map(|client| { - let mut grant = AuthorizationGrant::sample(now, rng); - // XXX - grant.client_id = client.id; + sample_list( + Client::samples(now, rng) + .into_iter() + .flat_map(|client| { + let mut grant = AuthorizationGrant::sample(now, rng); + // XXX + grant.client_id = client.id; - let authorization_grant = - PolicyViolationContext::for_authorization_grant(grant, client.clone()); - let device_code_grant = PolicyViolationContext::for_device_code_grant( - DeviceCodeGrant { - id: Ulid::from_datetime_with_source(now.into(), rng), - state: mas_data_model::DeviceCodeGrantState::Pending, - client_id: client.id, - scope: [OPENID].into_iter().collect(), - user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(), - device_code: Alphanumeric.sample_string(rng, 32), - created_at: now - Duration::try_minutes(5).unwrap(), - expires_at: now + Duration::try_minutes(25).unwrap(), - ip_address: None, - user_agent: None, - }, - client, - ); + let authorization_grant = + PolicyViolationContext::for_authorization_grant(grant, client.clone()); + let device_code_grant = PolicyViolationContext::for_device_code_grant( + DeviceCodeGrant { + id: Ulid::from_datetime_with_source(now.into(), rng), + state: mas_data_model::DeviceCodeGrantState::Pending, + client_id: client.id, + scope: [OPENID].into_iter().collect(), + user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(), + device_code: Alphanumeric.sample_string(rng, 32), + created_at: now - Duration::try_minutes(5).unwrap(), + expires_at: now + Duration::try_minutes(25).unwrap(), + ip_address: None, + user_agent: None, + }, + client, + ); - [authorization_grant, device_code_grant] - }) - .collect() + [authorization_grant, device_code_grant] + }) + .collect(), + ) } } @@ -778,18 +865,22 @@ pub struct CompatSsoContext { } impl TemplateContext for CompatSsoContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { let id = Ulid::from_datetime_with_source(now.into(), rng); - vec![CompatSsoContext::new(CompatSsoLogin { + 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, - })] + })]) } } @@ -836,11 +927,15 @@ impl EmailRecoveryContext { } impl TemplateContext for EmailRecoveryContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - User::samples(now, rng).into_iter().map(|user| { + sample_list(User::samples(now, rng).into_iter().map(|user| { let session = UserRecoverySession { id: Ulid::from_datetime_with_source(now.into(), rng), email: "hello@example.com".to_owned(), @@ -854,7 +949,7 @@ impl TemplateContext for EmailRecoveryContext { let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap(); Self::new(user, session, link) - }).collect() + }).collect()) } } @@ -897,28 +992,37 @@ impl EmailVerificationContext { } impl TemplateContext for EmailVerificationContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - BrowserSession::samples(now, rng) - .into_iter() - .map(|browser_session| { - let authentication_code = UserEmailAuthenticationCode { - id: Ulid::from_datetime_with_source(now.into(), rng), - user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng), - code: "123456".to_owned(), - created_at: now - Duration::try_minutes(5).unwrap(), - expires_at: now + Duration::try_minutes(25).unwrap(), - }; + sample_list( + BrowserSession::samples(now, rng) + .into_iter() + .map(|browser_session| { + let authentication_code = UserEmailAuthenticationCode { + id: Ulid::from_datetime_with_source(now.into(), rng), + user_email_authentication_id: Ulid::from_datetime_with_source( + now.into(), + rng, + ), + code: "123456".to_owned(), + created_at: now - Duration::try_minutes(5).unwrap(), + expires_at: now + Duration::try_minutes(25).unwrap(), + }; - Self { - browser_session: Some(browser_session), - user_registration: None, - authentication_code, - } - }) - .collect() + Self { + browser_session: Some(browser_session), + user_registration: None, + authentication_code, + } + }) + .collect(), + ) } } @@ -963,7 +1067,11 @@ impl RegisterStepsVerifyEmailContext { } impl TemplateContext for RegisterStepsVerifyEmailContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { @@ -976,10 +1084,10 @@ impl TemplateContext for RegisterStepsVerifyEmailContext { completed_at: None, }; - vec![Self { + sample_list(vec![Self { form: FormState::default(), authentication, - }] + }]) } } @@ -1003,13 +1111,13 @@ impl TemplateContext for RegisterStepsEmailInUseContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { let email = "hello@example.com".to_owned(); let action = PostAuthAction::continue_grant(Ulid::nil()); - vec![Self::new(email, Some(action))] + sample_list(vec![Self::new(email, Some(action))]) } } @@ -1058,13 +1166,13 @@ impl TemplateContext for RegisterStepsDisplayNameContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![Self { + sample_list(vec![Self { form: FormState::default(), - }] + }]) } } @@ -1113,13 +1221,13 @@ impl TemplateContext for RegisterStepsRegistrationTokenContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![Self { + sample_list(vec![Self { form: FormState::default(), - }] + }]) } } @@ -1164,11 +1272,11 @@ impl TemplateContext for RecoveryStartContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![ + sample_list(vec![ Self::new(), Self::new().with_form_state( FormState::default() @@ -1178,7 +1286,7 @@ impl TemplateContext for RecoveryStartContext { FormState::default() .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid), ), - ] + ]) } } @@ -1202,7 +1310,11 @@ impl RecoveryProgressContext { } impl TemplateContext for RecoveryProgressContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { @@ -1216,7 +1328,7 @@ impl TemplateContext for RecoveryProgressContext { consumed_at: None, }; - vec![ + sample_list(vec![ Self { session: session.clone(), resend_failed_due_to_rate_limit: false, @@ -1225,7 +1337,7 @@ impl TemplateContext for RecoveryProgressContext { session, resend_failed_due_to_rate_limit: true, }, - ] + ]) } } @@ -1244,7 +1356,11 @@ impl RecoveryExpiredContext { } impl TemplateContext for RecoveryExpiredContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { @@ -1258,10 +1374,9 @@ impl TemplateContext for RecoveryExpiredContext { consumed_at: None, }; - vec![Self { session }] + sample_list(vec![Self { session }]) } } - /// Fields of the account recovery finish form #[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -1305,30 +1420,36 @@ impl RecoveryFinishContext { } impl TemplateContext for RecoveryFinishContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - User::samples(now, rng) - .into_iter() - .flat_map(|user| { - vec![ - Self::new(user.clone()), - Self::new(user.clone()).with_form_state( - FormState::default().with_error_on_field( - RecoveryFinishFormField::NewPassword, - FieldError::Invalid, + sample_list( + User::samples(now, rng) + .into_iter() + .flat_map(|user| { + vec![ + Self::new(user.clone()), + Self::new(user.clone()).with_form_state( + FormState::default().with_error_on_field( + RecoveryFinishFormField::NewPassword, + FieldError::Invalid, + ), ), - ), - Self::new(user.clone()).with_form_state( - FormState::default().with_error_on_field( - RecoveryFinishFormField::NewPasswordConfirm, - FieldError::Invalid, + Self::new(user.clone()).with_form_state( + FormState::default().with_error_on_field( + RecoveryFinishFormField::NewPasswordConfirm, + FieldError::Invalid, + ), ), - ), - ] - }) - .collect() + ] + }) + .collect(), + ) } } @@ -1348,14 +1469,20 @@ impl UpstreamExistingLinkContext { } impl TemplateContext for UpstreamExistingLinkContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - User::samples(now, rng) - .into_iter() - .map(|linked_user| Self { linked_user }) - .collect() + sample_list( + User::samples(now, rng) + .into_iter() + .map(|linked_user| Self { linked_user }) + .collect(), + ) } } @@ -1380,12 +1507,16 @@ impl UpstreamSuggestLink { } impl TemplateContext for UpstreamSuggestLink { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { let id = Ulid::from_datetime_with_source(now.into(), rng); - vec![Self::for_link_id(id)] + sample_list(vec![Self::for_link_id(id)]) } } @@ -1505,11 +1636,15 @@ impl UpstreamRegister { } impl TemplateContext for UpstreamRegister { - fn sample(now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + _rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - vec![Self::new( + sample_list(vec![Self::new( UpstreamOAuthLink { id: Ulid::nil(), provider_id: Ulid::nil(), @@ -1545,7 +1680,7 @@ impl TemplateContext for UpstreamRegister { disabled_at: None, on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }, - )] + )]) } } @@ -1591,17 +1726,17 @@ impl TemplateContext for DeviceLinkContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![ + sample_list(vec![ Self::new(), Self::new().with_form_state( FormState::default() .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required), ), - ] + ]) } } @@ -1621,13 +1756,17 @@ impl DeviceConsentContext { } impl TemplateContext for DeviceConsentContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - Client::samples(now, rng) + sample_list(Client::samples(now, rng) .into_iter() - .map(|client| { + .map(|client| { let grant = DeviceCodeGrant { id: Ulid::from_datetime_with_source(now.into(), rng), state: mas_data_model::DeviceCodeGrantState::Pending, @@ -1642,7 +1781,7 @@ impl TemplateContext for DeviceConsentContext { }; Self { grant, client } }) - .collect() + .collect()) } } @@ -1662,14 +1801,20 @@ impl AccountInactiveContext { } impl TemplateContext for AccountInactiveContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - User::samples(now, rng) - .into_iter() - .map(|user| AccountInactiveContext { user }) - .collect() + sample_list( + User::samples(now, rng) + .into_iter() + .map(|user| AccountInactiveContext { user }) + .collect(), + ) } } @@ -1692,17 +1837,21 @@ impl DeviceNameContext { } impl TemplateContext for DeviceNameContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - Client::samples(now, rng) + sample_list(Client::samples(now, rng) .into_iter() .map(|client| DeviceNameContext { client, raw_user_agent: "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(), }) - .collect() + .collect()) } } @@ -1714,16 +1863,25 @@ pub struct FormPostContext { } impl TemplateContext for FormPostContext { - fn sample(now: chrono::DateTime, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec + fn sample( + now: chrono::DateTime, + rng: &mut impl Rng, + locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { let sample_params = T::sample(now, rng, locales); sample_params .into_iter() - .map(|params| FormPostContext { - redirect_uri: "https://example.com/callback".parse().ok(), - params, + .map(|(k, params)| { + ( + k, + FormPostContext { + redirect_uri: "https://example.com/callback".parse().ok(), + params, + }, + ) }) .collect() } @@ -1791,18 +1949,18 @@ impl TemplateContext for ErrorContext { _now: chrono::DateTime, _rng: &mut impl Rng, _locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - vec![ + sample_list(vec![ Self::new() .with_code("sample_error") .with_description("A fancy description".into()) .with_details("Something happened".into()), Self::new().with_code("another_error"), Self::new(), - ] + ]) } } @@ -1881,11 +2039,15 @@ impl NotFoundContext { } impl TemplateContext for NotFoundContext { - fn sample(_now: DateTime, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec + fn sample( + _now: DateTime, + _rng: &mut impl Rng, + _locales: &[DataLocale], + ) -> BTreeMap where Self: Sized, { - vec![ + sample_list(vec![ Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()), Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()), Self::new( @@ -1893,6 +2055,6 @@ impl TemplateContext for NotFoundContext { Version::HTTP_10, &"/foo?bar=baz".parse().unwrap(), ), - ] + ]) } } diff --git a/crates/templates/src/context/branding.rs b/crates/templates/src/context/branding.rs index eb7e3546a..15932567f 100644 --- a/crates/templates/src/context/branding.rs +++ b/crates/templates/src/context/branding.rs @@ -58,9 +58,9 @@ impl Object for SiteBranding { fn get_value(self: &Arc, name: &Value) -> Option { match name.as_str()? { "server_name" => Some(self.server_name.clone().into()), - "policy_uri" => self.policy_uri.clone().map(Value::from), - "tos_uri" => self.tos_uri.clone().map(Value::from), - "imprint" => self.imprint.clone().map(Value::from), + "policy_uri" => Some(Value::from(self.policy_uri.clone())), + "tos_uri" => Some(Value::from(self.tos_uri.clone())), + "imprint" => Some(Value::from(self.imprint.clone())), _ => None, } } diff --git a/crates/templates/src/context/captcha.rs b/crates/templates/src/context/captcha.rs index 442cea4f8..3daafb745 100644 --- a/crates/templates/src/context/captcha.rs +++ b/crates/templates/src/context/captcha.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::sync::Arc; +use std::{collections::BTreeMap, sync::Arc}; use mas_i18n::DataLocale; use minijinja::{ @@ -13,7 +13,7 @@ use minijinja::{ }; use serde::Serialize; -use crate::TemplateContext; +use crate::{TemplateContext, context::SampleIdentifier}; #[derive(Debug)] struct CaptchaConfig(mas_data_model::CaptchaConfig); @@ -62,14 +62,13 @@ impl TemplateContext for WithCaptcha { now: chrono::DateTime, rng: &mut impl rand::prelude::Rng, locales: &[DataLocale], - ) -> Vec + ) -> BTreeMap where Self: Sized, { - let inner = T::sample(now, rng, locales); - inner + T::sample(now, rng, locales) .into_iter() - .map(|inner| Self::new(None, inner)) + .map(|(k, inner)| (k, Self::new(None, inner))) .collect() } } diff --git a/crates/templates/src/context/ext.rs b/crates/templates/src/context/ext.rs index e4ae3886c..679ad91a7 100644 --- a/crates/templates/src/context/ext.rs +++ b/crates/templates/src/context/ext.rs @@ -45,6 +45,7 @@ impl SiteConfigExt for SiteConfig { fn templates_features(&self) -> SiteFeatures { SiteFeatures { password_registration: self.password_registration_enabled, + password_registration_email_required: self.password_registration_email_required, password_login: self.password_login_enabled, account_recovery: self.account_recovery_allowed, login_with_email_allowed: self.login_with_email_allowed, diff --git a/crates/templates/src/context/features.rs b/crates/templates/src/context/features.rs index d514b5c63..07e80f702 100644 --- a/crates/templates/src/context/features.rs +++ b/crates/templates/src/context/features.rs @@ -18,6 +18,9 @@ pub struct SiteFeatures { /// Whether local password-based registration is enabled. pub password_registration: bool, + /// Whether local password-based registration requires an email address. + pub password_registration_email_required: bool, + /// Whether local password-based login is enabled. pub password_login: bool, @@ -32,6 +35,9 @@ impl Object for SiteFeatures { fn get_value(self: &Arc, field: &Value) -> Option { match field.as_str()? { "password_registration" => Some(Value::from(self.password_registration)), + "password_registration_email_required" => { + Some(Value::from(self.password_registration_email_required)) + } "password_login" => Some(Value::from(self.password_login)), "account_recovery" => Some(Value::from(self.account_recovery)), "login_with_email_allowed" => Some(Value::from(self.login_with_email_allowed)), @@ -42,6 +48,7 @@ impl Object for SiteFeatures { fn enumerate(self: &Arc) -> Enumerator { Enumerator::Str(&[ "password_registration", + "password_registration_email_required", "password_login", "account_recovery", "login_with_email_allowed", diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 1c0bef423..603dcfdf6 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -9,7 +9,10 @@ //! Templates rendering -use std::{collections::HashSet, sync::Arc}; +use std::{ + collections::{BTreeMap, HashSet}, + sync::Arc, +}; use anyhow::Context as _; use arc_swap::ArcSwap; @@ -17,7 +20,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use mas_i18n::Translator; use mas_router::UrlBuilder; use mas_spa::ViteManifest; -use minijinja::Value; +use minijinja::{UndefinedBehavior, Value}; use rand::Rng; use serde::Serialize; use thiserror::Error; @@ -50,6 +53,7 @@ pub use self::{ }, forms::{FieldError, FormError, FormField, FormState, ToFormState}, }; +use crate::context::SampleIdentifier; /// Escape the given string for use in HTML /// @@ -71,6 +75,9 @@ pub struct Templates { vite_manifest_path: Utf8PathBuf, translations_path: Utf8PathBuf, path: Utf8PathBuf, + /// Whether template rendering is in strict mode (for testing, + /// until this can be rolled out in production.) + strict: bool, } /// There was an issue while loading the templates @@ -151,6 +158,7 @@ impl Templates { translations_path: Utf8PathBuf, branding: SiteBranding, features: SiteFeatures, + strict: bool, ) -> Result { let (translator, environment) = Self::load_( &path, @@ -159,6 +167,7 @@ impl Templates { &translations_path, branding.clone(), features, + strict, ) .await?; Ok(Self { @@ -170,6 +179,7 @@ impl Templates { translations_path, branding, features, + strict, }) } @@ -180,6 +190,7 @@ impl Templates { translations_path: &Utf8Path, branding: SiteBranding, features: SiteFeatures, + strict: bool, ) -> Result<(Arc, Arc>), TemplateLoadingError> { let path = path.to_owned(); let span = tracing::Span::current(); @@ -205,6 +216,15 @@ impl Templates { span.in_scope(move || { let mut loaded: HashSet<_> = HashSet::new(); let mut env = minijinja::Environment::new(); + // Don't allow use of undefined variables + env.set_undefined_behavior(if strict { + UndefinedBehavior::Strict + } else { + // For now, allow semi-strict, because we don't have total test coverage of + // tests and some tests rely on if conditions against sometimes-undefined + // variables + UndefinedBehavior::SemiStrict + }); let root = path.canonicalize_utf8()?; info!(%root, "Loading templates from filesystem"); for entry in walkdir::WalkDir::new(&root) @@ -275,6 +295,7 @@ impl Templates { &self.translations_path, self.branding.clone(), self.features, + self.strict, ) .await?; @@ -383,7 +404,7 @@ register_templates! { pub fn render_recovery_disabled(WithLanguage) { "pages/recovery/disabled.html" } /// Render the form used by the `form_post` response mode - pub fn render_form_post(WithLanguage>) { "form_post.html" } + pub fn render_form_post<#[sample(EmptyContext)] T: Serialize>(WithLanguage>) { "form_post.html" } /// Render the HTML error page pub fn render_error(ErrorContext) { "pages/error.html" } @@ -439,7 +460,13 @@ register_templates! { impl Templates { /// Render all templates with the generated samples to check if they render - /// properly + /// properly. + /// + /// Returns the renders in a map whose keys are template names + /// and the values are lists of renders (according to the list + /// of samples). + /// Samples are stable across re-runs and can be used for + /// acceptance testing. /// /// # Errors /// @@ -448,47 +475,8 @@ impl Templates { &self, now: chrono::DateTime, rng: &mut impl Rng, - ) -> anyhow::Result<()> { - check::render_not_found(self, now, rng)?; - check::render_app(self, now, rng)?; - check::render_swagger(self, now, rng)?; - check::render_swagger_callback(self, now, rng)?; - check::render_login(self, now, rng)?; - check::render_register(self, now, rng)?; - check::render_password_register(self, now, rng)?; - check::render_register_steps_verify_email(self, now, rng)?; - check::render_register_steps_email_in_use(self, now, rng)?; - check::render_register_steps_display_name(self, now, rng)?; - check::render_register_steps_registration_token(self, now, rng)?; - check::render_consent(self, now, rng)?; - check::render_policy_violation(self, now, rng)?; - check::render_sso_login(self, now, rng)?; - check::render_index(self, now, rng)?; - check::render_recovery_start(self, now, rng)?; - check::render_recovery_progress(self, now, rng)?; - check::render_recovery_finish(self, now, rng)?; - check::render_recovery_expired(self, now, rng)?; - check::render_recovery_consumed(self, now, rng)?; - check::render_recovery_disabled(self, now, rng)?; - check::render_form_post::(self, now, rng)?; - check::render_error(self, now, rng)?; - check::render_email_recovery_txt(self, now, rng)?; - check::render_email_recovery_html(self, now, rng)?; - check::render_email_recovery_subject(self, now, rng)?; - check::render_email_verification_txt(self, now, rng)?; - check::render_email_verification_html(self, now, rng)?; - check::render_email_verification_subject(self, now, rng)?; - check::render_upstream_oauth2_link_mismatch(self, now, rng)?; - check::render_upstream_oauth2_login_link(self, now, rng)?; - check::render_upstream_oauth2_suggest_link(self, now, rng)?; - check::render_upstream_oauth2_do_register(self, now, rng)?; - check::render_device_link(self, now, rng)?; - check::render_device_consent(self, now, rng)?; - check::render_account_deactivated(self, now, rng)?; - check::render_account_locked(self, now, rng)?; - check::render_account_logged_out(self, now, rng)?; - check::render_device_name(self, now, rng)?; - Ok(()) + ) -> anyhow::Result> { + check::all(self, now, rng) } } @@ -509,6 +497,7 @@ mod tests { let features = SiteFeatures { password_login: true, password_registration: true, + password_registration_email_required: true, account_recovery: true, login_with_email_allowed: true, }; @@ -523,6 +512,8 @@ mod tests { translations_path, branding, features, + // Use strict mode in tests + true, ) .await .unwrap(); diff --git a/crates/templates/src/macros.rs b/crates/templates/src/macros.rs index e0b203735..95b57f0d9 100644 --- a/crates/templates/src/macros.rs +++ b/crates/templates/src/macros.rs @@ -31,7 +31,9 @@ macro_rules! register_templates { pub fn $name:ident // Optional list of generics. Taken from // https://newbedev.com/rust-macro-accepting-type-with-generic-parameters - $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? + // For sample rendering, we also require a 'sample' generic parameter to be provided, + // using #[sample(Type)] attribute syntax + $(< $( #[sample( $generic_default:tt )] $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? // Type of context taken by the template ( $param:ty ) { @@ -69,28 +71,53 @@ macro_rules! register_templates { pub mod check { use super::*; + /// Check and render all templates with all samples. + /// + /// Returns the sample renders. The keys in the map are the template names. + /// + /// # Errors + /// + /// Returns an error if any template fails to render with any of the sample. + pub(crate) fn all(templates: &Templates, now: chrono::DateTime, rng: &mut impl rand::Rng) -> anyhow::Result<::std::collections::BTreeMap<(&'static str, SampleIdentifier), String>> { + let mut out = ::std::collections::BTreeMap::new(); + // TODO shouldn't the Rng be independent for each render? + $( + out.extend( + $name $(::< $( $generic_default ),* >)? (templates, now, rng)? + .into_iter() + .map(|(sample_identifier, rendered)| (($template, sample_identifier), rendered)) + ); + )* + + Ok(out) + } + $( #[doc = concat!("Render the `", $template, "` template with sample contexts")] /// + /// Returns the sample renders. + /// /// # Errors /// /// Returns an error if the template fails to render with any of the sample. pub(crate) fn $name $(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)? (templates: &Templates, now: chrono::DateTime, rng: &mut impl rand::Rng) - -> anyhow::Result<()> { + -> anyhow::Result> { let locales = templates.translator().available_locales(); - let samples: Vec< $param > = TemplateContext::sample(now, rng, &locales); + let samples: BTreeMap = TemplateContext::sample(now, rng, &locales); let name = $template; - for sample in samples { + let mut out = BTreeMap::new(); + for (sample_identifier, sample) in samples { let context = serde_json::to_value(&sample)?; ::tracing::info!(name, %context, "Rendering template"); - templates. $name (&sample) - .with_context(|| format!("Failed to render template {:?} with context {}", name, context))?; + let rendered = templates. $name (&sample) + .with_context(|| format!("Failed to render sample template {name:?}-{sample_identifier:?} with context {context}"))?; + out.insert(sample_identifier, rendered); } - Ok(()) + Ok(out) } )* } diff --git a/deny.toml b/deny.toml index 508816d14..1671119ca 100644 --- a/deny.toml +++ b/deny.toml @@ -59,11 +59,7 @@ deny = [ multiple-versions = "deny" skip = [ - { name = "regex-syntax", version = "0.6.29" }, # tracing-subscriber[env-filter] -> matchers depends on the old version - { name = "regex-automata", version = "0.1.10" }, # ^ { name = "itertools", version = "0.13.0" }, # zxcvbn depends on this old version - { name = "indexmap", version = "1.9.3" }, # schemars depends on this old version - { name = "hashbrown", version = "0.12.3" }, # schemars -> indexmap depends on this old version { name = "hashbrown", version = "0.14.5" }, # a few crates depend on this old version # a few dependencies depend on the 1.x version of thiserror { name = "thiserror", version = "1.0.69" }, @@ -75,10 +71,6 @@ skip = [ # cron depends on this old version # https://github.com/zslayton/cron/pull/137 { name = "winnow", version = "0.6.20" }, - # opa-wasm -> wasmtime -> memfd depends on this old version - # https://github.com/lucab/memfd-rs/pull/72 - { name = "rustix", version = "0.38.44" }, - { name = "linux-raw-sys", version = "0.9.4" }, # We are still mainly using rand 0.8 { name = "rand", version = "0.8.5" }, @@ -98,8 +90,3 @@ deny = ["oldtime"] unknown-registry = "warn" unknown-git = "warn" allow-registry = ["https://github.com/rust-lang/crates.io-index"] - -allow-git = [ - # https://github.com/open-telemetry/opentelemetry-rust/pull/3076 - "https://github.com/sandhose/opentelemetry-rust", -] diff --git a/docs/api/spec.json b/docs/api/spec.json index 9e3e336aa..ac56910b8 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -16,6 +16,65 @@ } ], "paths": { + "/api/admin/v1/site-config": { + "get": { + "tags": [ + "server" + ], + "summary": "Get informations about the configuration of this MAS instance", + "operationId": "siteConfig", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiteConfig" + }, + "example": { + "server_name": "example.com", + "password_login_enabled": true, + "password_registration_enabled": true, + "password_registration_email_required": true, + "registration_token_required": true, + "email_change_allowed": true, + "displayname_change_allowed": true, + "password_change_allowed": true, + "account_recovery_allowed": true, + "account_deactivation_allowed": true, + "captcha_enabled": true, + "minimum_password_complexity": 3 + } + } + } + } + } + } + }, + "/api/admin/v1/version": { + "get": { + "tags": [ + "server" + ], + "summary": "Get the version currently running", + "operationId": "version", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Version" + }, + "example": { + "version": "v1.0.0" + } + } + } + } + } + } + }, "/api/admin/v1/compat-sessions": { "get": { "tags": [ @@ -31,8 +90,14 @@ "description": "Retrieve the items before the given ID", "schema": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -42,8 +107,14 @@ "description": "Retrieve the items after the given ID", "schema": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -53,10 +124,12 @@ "description": "Retrieve the first N items", "schema": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "style": "form" }, @@ -66,10 +139,29 @@ "description": "Retrieve the last N items", "schema": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -79,8 +171,14 @@ "description": "Retrieve the items for the given user", "schema": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -90,19 +188,31 @@ "description": "Retrieve the items started from the given browser session", "schema": { "description": "Retrieve the items started from the given browser session", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, { "in": "query", "name": "filter[status]", - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", "schema": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", - "$ref": "#/components/schemas/CompatSessionStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", + "anyOf": [ + { + "$ref": "#/components/schemas/CompatSessionStatus" + }, + { + "type": "null" + } + ] }, "style": "form" } @@ -137,6 +247,11 @@ }, "links": { "self": "/api/admin/v1/compat-sessions/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -156,6 +271,11 @@ }, "links": { "self": "/api/admin/v1/compat-sessions/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -175,6 +295,11 @@ }, "links": { "self": "/api/admin/v1/compat-sessions/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -282,6 +407,98 @@ } } }, + "/api/admin/v1/compat-sessions/{id}/finish": { + "post": { + "tags": [ + "compat-session" + ], + "summary": "Finish a compatibility session", + "description": "Calling this endpoint will finish the compatibility session, preventing any further use. A job will be scheduled to sync the user's devices with the homeserver.", + "operationId": "finishCompatSession", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "Compatibility session was finished", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_CompatSession" + }, + "example": { + "data": { + "type": "compat-session", + "id": "02081040G2081040G2081040G2", + "attributes": { + "user_id": "01040G2081040G2081040G2081", + "device_id": "FFGGHHIIJJ", + "user_session_id": "0J289144GJ289144GJ289144GJ", + "redirect_uri": null, + "created_at": "1970-01-01T00:00:00Z", + "user_agent": "Mozilla/5.0", + "last_active_at": "1970-01-01T00:00:00Z", + "last_active_ip": "1.2.3.4", + "finished_at": "1970-01-01T00:00:00Z", + "human_name": null + }, + "links": { + "self": "/api/admin/v1/compat-sessions/02081040G2081040G2081040G2" + } + }, + "links": { + "self": "/api/admin/v1/compat-sessions/02081040G2081040G2081040G2/finish" + } + } + } + } + }, + "400": { + "description": "Session is already finished", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "Compatibility session with ID 00000000000000000000000000 is already finished" + } + ] + } + } + } + }, + "404": { + "description": "Compatibility session was not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "Compatibility session with ID 00000000000000000000000000 not found" + } + ] + } + } + } + } + } + } + }, "/api/admin/v1/oauth2-sessions": { "get": { "tags": [ @@ -297,8 +514,14 @@ "description": "Retrieve the items before the given ID", "schema": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -308,8 +531,14 @@ "description": "Retrieve the items after the given ID", "schema": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -319,10 +548,12 @@ "description": "Retrieve the first N items", "schema": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "style": "form" }, @@ -332,10 +563,29 @@ "description": "Retrieve the last N items", "schema": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -345,8 +595,14 @@ "description": "Retrieve the items for the given user", "schema": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -356,8 +612,14 @@ "description": "Retrieve the items for the given client", "schema": { "description": "Retrieve the items for the given client", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -367,8 +629,14 @@ "description": "Retrieve the items only for a specific client kind", "schema": { "description": "Retrieve the items only for a specific client kind", - "$ref": "#/components/schemas/OAuth2ClientKind", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/OAuth2ClientKind" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -378,8 +646,14 @@ "description": "Retrieve the items started from the given browser session", "schema": { "description": "Retrieve the items started from the given browser session", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -389,22 +663,28 @@ "description": "Retrieve the items with the given scope", "schema": { "description": "Retrieve the items with the given scope", - "default": [], "type": "array", "items": { "type": "string" - } + }, + "default": [] }, "style": "form" }, { "in": "query", "name": "filter[status]", - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", "schema": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", - "$ref": "#/components/schemas/OAuth2SessionStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", + "anyOf": [ + { + "$ref": "#/components/schemas/OAuth2SessionStatus" + }, + { + "type": "null" + } + ] }, "style": "form" } @@ -439,6 +719,11 @@ }, "links": { "self": "/api/admin/v1/oauth2-sessions/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -458,6 +743,11 @@ }, "links": { "self": "/api/admin/v1/oauth2-sessions/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -477,6 +767,11 @@ }, "links": { "self": "/api/admin/v1/oauth2-sessions/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -601,6 +896,716 @@ } } }, + "/api/admin/v1/oauth2-sessions/{id}/finish": { + "post": { + "tags": [ + "oauth2-session" + ], + "summary": "Finish an OAuth 2.0 session", + "description": "Calling this endpoint will finish the OAuth 2.0 session, preventing any further use. If the session has a user associated with it, a job will be scheduled to sync the user's devices with the homeserver.", + "operationId": "finishOAuth2Session", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "OAuth 2.0 session was finished", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_OAuth2Session" + }, + "example": { + "data": { + "type": "oauth2-session", + "id": "030C1G60R30C1G60R30C1G60R3", + "attributes": { + "created_at": "1970-01-01T00:00:00Z", + "finished_at": "1970-01-01T00:00:00Z", + "user_id": "040G2081040G2081040G208104", + "user_session_id": "050M2GA1850M2GA1850M2GA185", + "client_id": "060R30C1G60R30C1G60R30C1G6", + "scope": "urn:matrix:client:api:*", + "user_agent": "Mozilla/5.0", + "last_active_at": "1970-01-01T00:00:00Z", + "last_active_ip": "127.0.0.1", + "human_name": null + }, + "links": { + "self": "/api/admin/v1/oauth2-sessions/030C1G60R30C1G60R30C1G60R3" + } + }, + "links": { + "self": "/api/admin/v1/oauth2-sessions/030C1G60R30C1G60R30C1G60R3/finish" + } + } + } + } + }, + "400": { + "description": "Session is already finished", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "OAuth 2.0 session with ID 00000000000000000000000000 is already finished" + } + ] + } + } + } + }, + "404": { + "description": "OAuth 2.0 session was not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "OAuth 2.0 session with ID 00000000000000000000000000 not found" + } + ] + } + } + } + } + } + } + }, + "/api/admin/v1/personal-sessions": { + "get": { + "tags": [ + "personal-session" + ], + "summary": "List personal sessions", + "description": "Retrieve a list of personal sessions.\nNote that by default, all sessions, including revoked ones are returned, with the oldest first.\nUse the `filter[status]` parameter to filter the sessions by their status and `page[last]` parameter to retrieve the last N sessions.", + "operationId": "listPersonalSessions", + "parameters": [ + { + "in": "query", + "name": "page[before]", + "description": "Retrieve the items before the given ID", + "schema": { + "description": "Retrieve the items before the given ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "page[after]", + "description": "Retrieve the items after the given ID", + "schema": { + "description": "Retrieve the items after the given ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "page[first]", + "description": "Retrieve the first N items", + "schema": { + "description": "Retrieve the first N items", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "page[last]", + "description": "Retrieve the last N items", + "schema": { + "description": "Retrieve the last N items", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[owner_user]", + "description": "Filter by owner user ID", + "schema": { + "description": "Filter by owner user ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[owner_client]", + "description": "Filter by owner `OAuth2` client ID", + "schema": { + "description": "Filter by owner `OAuth2` client ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[actor_user]", + "description": "Filter by actor user ID", + "schema": { + "description": "Filter by actor user ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[scope]", + "description": "Retrieve the items with the given scope", + "schema": { + "description": "Retrieve the items with the given scope", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[status]", + "description": "Filter by session status", + "schema": { + "description": "Filter by session status", + "anyOf": [ + { + "$ref": "#/components/schemas/PersonalSessionStatus" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[expires_before]", + "description": "Filter by access token expiry date", + "schema": { + "description": "Filter by access token expiry date", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[expires_after]", + "description": "Filter by access token expiry date", + "schema": { + "description": "Filter by access token expiry date", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[expires]", + "description": "Filter by whether the access token has an expiry time", + "schema": { + "description": "Filter by whether the access token has an expiry time", + "type": [ + "boolean", + "null" + ] + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "Paginated response of personal sessions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_for_PersonalSession" + }, + "example": { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "personal-session", + "id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "attributes": { + "created_at": "2022-01-16T13:00:00Z", + "revoked_at": null, + "owner_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_name": "Alice's Development Token", + "scope": "openid urn:matrix:org.matrix.msc2967.client:api:*", + "last_active_at": "2022-01-16T15:30:00Z", + "last_active_ip": "192.168.1.100", + "expires_at": null + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + } + }, + { + "type": "personal-session", + "id": "01FSHN9AG0BJ6AC5HQ9X6H4RP5", + "attributes": { + "created_at": "2022-01-16T13:01:00Z", + "revoked_at": "2022-01-16T16:20:00Z", + "owner_user_id": "01FSHN9AG0NZAA6S4AF7CTV32F", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG0NZAA6S4AF7CTV32F", + "human_name": "Bob's Mobile App", + "scope": "openid", + "last_active_at": "2022-01-16T16:03:20Z", + "last_active_ip": "10.0.0.50", + "expires_at": null + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0BJ6AC5HQ9X6H4RP5" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0BJ6AC5HQ9X6H4RP5" + } + } + }, + { + "type": "personal-session", + "id": "01FSHN9AG0CJ6AC5HQ9X6H4RP6", + "attributes": { + "created_at": "2022-01-16T13:02:00Z", + "revoked_at": null, + "owner_user_id": null, + "owner_client_id": "01FSHN9AG0DJ6AC5HQ9X6H4RP7", + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_name": "CI/CD Pipeline Token", + "scope": "openid urn:mas:admin", + "last_active_at": "2022-01-16T15:46:40Z", + "last_active_ip": "203.0.113.10", + "expires_at": "2022-01-24T04:36:40Z" + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0CJ6AC5HQ9X6H4RP6" + }, + "meta": { + "page": { + "cursor": "01FSHN9AG0CJ6AC5HQ9X6H4RP6" + } + } + } + ], + "links": { + "self": "/api/admin/v1/personal-sessions?page[first]=3", + "first": "/api/admin/v1/personal-sessions?page[first]=3", + "last": "/api/admin/v1/personal-sessions?page[last]=3", + "next": "/api/admin/v1/personal-sessions?page[after]=01FSHN9AG0CJ6AC5HQ9X6H4RP6&page[first]=3" + } + } + } + } + }, + "404": { + "description": "Client was not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "Client ID 00000000000000000000000000 not found" + } + ] + } + } + } + } + } + }, + "post": { + "tags": [ + "personal-session" + ], + "summary": "Create a new personal session with personal access token", + "operationId": "createPersonalSession", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePersonalSessionRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Personal session and personal access token were created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_PersonalSession" + } + } + } + }, + "400": { + "description": "Invalid scope provided", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "Invalid scope" + } + ] + } + } + } + }, + "404": { + "description": "User was not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "User not found" + } + ] + } + } + } + } + } + } + }, + "/api/admin/v1/personal-sessions/{id}": { + "get": { + "tags": [ + "personal-session" + ], + "summary": "Get a personal session", + "operationId": "getPersonalSession", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "Personal session details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_PersonalSession" + }, + "example": { + "data": { + "type": "personal-session", + "id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "attributes": { + "created_at": "2022-01-16T13:00:00Z", + "revoked_at": null, + "owner_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_name": "Alice's Development Token", + "scope": "openid urn:matrix:org.matrix.msc2967.client:api:*", + "last_active_at": "2022-01-16T15:30:00Z", + "last_active_ip": "192.168.1.100", + "expires_at": null + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + } + } + } + }, + "404": { + "description": "Personal session not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "Personal session not found" + } + ] + } + } + } + } + } + } + }, + "/api/admin/v1/personal-sessions/{id}/revoke": { + "post": { + "tags": [ + "personal-session" + ], + "summary": "Revoke a personal session", + "operationId": "revokePersonalSession", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "Personal session was revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_PersonalSession" + }, + "example": { + "data": { + "type": "personal-session", + "id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4", + "attributes": { + "created_at": "2022-01-16T13:00:00Z", + "revoked_at": null, + "owner_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "owner_client_id": null, + "actor_user_id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "human_name": "Alice's Development Token", + "scope": "openid urn:matrix:org.matrix.msc2967.client:api:*", + "last_active_at": "2022-01-16T15:30:00Z", + "last_active_ip": "192.168.1.100", + "expires_at": null + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + }, + "links": { + "self": "/api/admin/v1/personal-sessions/01FSHN9AG0AJ6AC5HQ9X6H4RP4" + } + } + } + } + }, + "404": { + "description": "Personal session not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "Personal session with ID 00000000000000000000000000 not found" + } + ] + } + } + } + }, + "409": { + "description": "Personal session already revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "Personal session with ID 00000000000000000000000000 is already revoked" + } + ] + } + } + } + } + } + } + }, + "/api/admin/v1/personal-sessions/{id}/regenerate": { + "post": { + "tags": [ + "personal-session" + ], + "summary": "Regenerate a personal session by replacing its personal access token", + "operationId": "regeneratePersonalSession", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegeneratePersonalSessionRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Personal session was regenerated and a personal access token was created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_PersonalSession" + } + } + } + }, + "404": { + "description": "User was not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "User not found" + } + ] + } + } + } + } + } + } + }, "/api/admin/v1/policy-data": { "post": { "tags": [ @@ -817,8 +1822,14 @@ "description": "Retrieve the items before the given ID", "schema": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -828,8 +1839,14 @@ "description": "Retrieve the items after the given ID", "schema": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -839,10 +1856,12 @@ "description": "Retrieve the first N items", "schema": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "style": "form" }, @@ -852,10 +1871,29 @@ "description": "Retrieve the last N items", "schema": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -865,19 +1903,53 @@ "description": "Retrieve users with (or without) the `admin` flag set", "schema": { "description": "Retrieve users with (or without) the `admin` flag set", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[legacy-guest]", + "description": "Retrieve users with (or without) the `legacy_guest` flag set", + "schema": { + "description": "Retrieve users with (or without) the `legacy_guest` flag set", + "type": [ + "boolean", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[search]", + "description": "Retrieve users where the username matches contains the given string\n\n Note that this doesn't change the ordering of the result, which are\n still ordered by ID.", + "schema": { + "description": "Retrieve users where the username matches contains the given string\n\n Note that this doesn't change the ordering of the result, which are\n still ordered by ID.", + "type": [ + "string", + "null" + ] }, "style": "form" }, { "in": "query", "name": "filter[status]", - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all users, including locked ones.\n\n* `active`: Only retrieve active users\n\n* `locked`: Only retrieve locked users (includes deactivated users)\n\n* `deactivated`: Only retrieve deactivated users", + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all users, including locked ones.\n\n * `active`: Only retrieve active users\n\n * `locked`: Only retrieve locked users (includes deactivated users)\n\n * `deactivated`: Only retrieve deactivated users", "schema": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all users, including locked ones.\n\n* `active`: Only retrieve active users\n\n* `locked`: Only retrieve locked users (includes deactivated users)\n\n* `deactivated`: Only retrieve deactivated users", - "$ref": "#/components/schemas/UserStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all users, including locked ones.\n\n * `active`: Only retrieve active users\n\n * `locked`: Only retrieve locked users (includes deactivated users)\n\n * `deactivated`: Only retrieve deactivated users", + "anyOf": [ + { + "$ref": "#/components/schemas/UserStatus" + }, + { + "type": "null" + } + ] }, "style": "form" } @@ -903,10 +1975,16 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -917,10 +1995,16 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": true + "admin": true, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -931,10 +2015,16 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": "1970-01-01T00:00:00Z", "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": true }, "links": { "self": "/api/admin/v1/users/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -983,7 +2073,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1069,7 +2160,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1226,7 +2318,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1306,7 +2399,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": true + "admin": true, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/02081040G2081040G2081040G2" @@ -1385,7 +2479,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": "1970-01-01T00:00:00Z", "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": true }, "links": { "self": "/api/admin/v1/users/030C1G60R30C1G60R30C1G60R3" @@ -1455,7 +2550,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1525,7 +2621,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": "1970-01-01T00:00:00Z", "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": true }, "links": { "self": "/api/admin/v1/users/030C1G60R30C1G60R30C1G60R3" @@ -1595,7 +2692,8 @@ "created_at": "1970-01-01T00:00:00Z", "locked_at": null, "deactivated_at": null, - "admin": false + "admin": false, + "legacy_guest": false }, "links": { "self": "/api/admin/v1/users/01040G2081040G2081040G2081" @@ -1643,8 +2741,14 @@ "description": "Retrieve the items before the given ID", "schema": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -1654,8 +2758,14 @@ "description": "Retrieve the items after the given ID", "schema": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -1665,10 +2775,12 @@ "description": "Retrieve the first N items", "schema": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "style": "form" }, @@ -1678,10 +2790,29 @@ "description": "Retrieve the last N items", "schema": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -1691,8 +2822,14 @@ "description": "Retrieve the items for the given user", "schema": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -1702,8 +2839,10 @@ "description": "Retrieve the user email with the given email address", "schema": { "description": "Retrieve the user email with the given email address", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "style": "form" } @@ -1731,6 +2870,11 @@ }, "links": { "self": "/api/admin/v1/user-emails/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } } ], @@ -1988,8 +3132,14 @@ "description": "Retrieve the items before the given ID", "schema": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -1999,8 +3149,14 @@ "description": "Retrieve the items after the given ID", "schema": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2010,10 +3166,12 @@ "description": "Retrieve the first N items", "schema": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "style": "form" }, @@ -2023,10 +3181,29 @@ "description": "Retrieve the last N items", "schema": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2036,19 +3213,31 @@ "description": "Retrieve the items for the given user", "schema": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, { "in": "query", "name": "filter[status]", - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", "schema": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", - "$ref": "#/components/schemas/UserSessionStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", + "anyOf": [ + { + "$ref": "#/components/schemas/UserSessionStatus" + }, + { + "type": "null" + } + ] }, "style": "form" } @@ -2079,6 +3268,11 @@ }, "links": { "self": "/api/admin/v1/user-sessions/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -2094,6 +3288,11 @@ }, "links": { "self": "/api/admin/v1/user-sessions/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -2109,6 +3308,11 @@ }, "links": { "self": "/api/admin/v1/user-sessions/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -2212,6 +3416,94 @@ } } }, + "/api/admin/v1/user-sessions/{id}/finish": { + "post": { + "tags": [ + "user-session" + ], + "summary": "Finish a user session", + "description": "Calling this endpoint will finish the user session, preventing any further use.", + "operationId": "finishUserSession", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "User session was finished", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_UserSession" + }, + "example": { + "data": { + "type": "user-session", + "id": "030C1G60R30C1G60R30C1G60R3", + "attributes": { + "created_at": "1970-01-01T00:00:00Z", + "finished_at": "1970-01-01T00:00:00Z", + "user_id": "040G2081040G2081040G208104", + "user_agent": "Mozilla/5.0", + "last_active_at": "1970-01-01T00:00:00Z", + "last_active_ip": "127.0.0.1" + }, + "links": { + "self": "/api/admin/v1/user-sessions/030C1G60R30C1G60R30C1G60R3" + } + }, + "links": { + "self": "/api/admin/v1/user-sessions/030C1G60R30C1G60R30C1G60R3/finish" + } + } + } + } + }, + "400": { + "description": "Session is already finished", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "User session with ID 00000000000000000000000000 is already finished" + } + ] + } + } + } + }, + "404": { + "description": "User session was not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "User session with ID 00000000000000000000000000 not found" + } + ] + } + } + } + } + } + } + }, "/api/admin/v1/user-registration-tokens": { "get": { "tags": [ @@ -2226,8 +3518,14 @@ "description": "Retrieve the items before the given ID", "schema": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2237,8 +3535,14 @@ "description": "Retrieve the items after the given ID", "schema": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2248,10 +3552,12 @@ "description": "Retrieve the first N items", "schema": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "style": "form" }, @@ -2261,10 +3567,29 @@ "description": "Retrieve the last N items", "schema": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2274,8 +3599,10 @@ "description": "Retrieve tokens that have (or have not) been used at least once", "schema": { "description": "Retrieve tokens that have (or have not) been used at least once", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] }, "style": "form" }, @@ -2285,8 +3612,10 @@ "description": "Retrieve tokens that are (or are not) revoked", "schema": { "description": "Retrieve tokens that are (or are not) revoked", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] }, "style": "form" }, @@ -2296,19 +3625,23 @@ "description": "Retrieve tokens that are (or are not) expired", "schema": { "description": "Retrieve tokens that are (or are not) expired", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] }, "style": "form" }, { "in": "query", "name": "filter[valid]", - "description": "Retrieve tokens that are (or are not) valid\n\nValid means that the token has not expired, is not revoked, and has not reached its usage limit.", + "description": "Retrieve tokens that are (or are not) valid\n\n Valid means that the token has not expired, is not revoked, and has not\n reached its usage limit.", "schema": { - "description": "Retrieve tokens that are (or are not) valid\n\nValid means that the token has not expired, is not revoked, and has not reached its usage limit.", - "type": "boolean", - "nullable": true + "description": "Retrieve tokens that are (or are not) valid\n\n Valid means that the token has not expired, is not revoked, and has not\n reached its usage limit.", + "type": [ + "boolean", + "null" + ] }, "style": "form" } @@ -2341,6 +3674,11 @@ }, "links": { "self": "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -2358,6 +3696,11 @@ }, "links": { "self": "/api/admin/v1/user-registration-tokens/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } } ], @@ -2773,8 +4116,14 @@ "description": "Retrieve the items before the given ID", "schema": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2784,8 +4133,14 @@ "description": "Retrieve the items after the given ID", "schema": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2795,10 +4150,12 @@ "description": "Retrieve the first N items", "schema": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "style": "form" }, @@ -2808,10 +4165,29 @@ "description": "Retrieve the last N items", "schema": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2821,8 +4197,14 @@ "description": "Retrieve the items for the given user", "schema": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2832,8 +4214,14 @@ "description": "Retrieve the items for the given provider", "schema": { "description": "Retrieve the items for the given provider", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "style": "form" }, @@ -2843,8 +4231,10 @@ "description": "Retrieve the items with the given subject", "schema": { "description": "Retrieve the items with the given subject", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "style": "form" } @@ -2874,6 +4264,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-links/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } } }, { @@ -2888,6 +4283,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-links/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } } }, { @@ -2902,6 +4302,11 @@ }, "links": { "self": "/api/admin/v1/upstream-oauth-links/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } } } ], @@ -3155,6 +4560,255 @@ } } } + }, + "/api/admin/v1/upstream-oauth-providers": { + "get": { + "tags": [ + "upstream-oauth-provider" + ], + "summary": "List upstream OAuth 2.0 providers", + "operationId": "listUpstreamOAuthProviders", + "parameters": [ + { + "in": "query", + "name": "page[before]", + "description": "Retrieve the items before the given ID", + "schema": { + "description": "Retrieve the items before the given ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "page[after]", + "description": "Retrieve the items after the given ID", + "schema": { + "description": "Retrieve the items after the given ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "page[first]", + "description": "Retrieve the first N items", + "schema": { + "description": "Retrieve the first N items", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "page[last]", + "description": "Retrieve the last N items", + "schema": { + "description": "Retrieve the last N items", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[enabled]", + "description": "Retrieve providers that are (or are not) enabled", + "schema": { + "description": "Retrieve providers that are (or are not) enabled", + "type": [ + "boolean", + "null" + ] + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "Paginated response of upstream OAuth 2.0 providers", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_for_UpstreamOAuthProvider" + }, + "example": { + "meta": { + "count": 42 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01040G2081040G2081040G2081", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "1970-01-01T00:00:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081" + }, + "meta": { + "page": { + "cursor": "01040G2081040G2081040G2081" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "02081040G2081040G2081040G2", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "1970-01-01T00:00:00Z", + "disabled_at": "1970-01-01T00:00:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/02081040G2081040G2081040G2" + }, + "meta": { + "page": { + "cursor": "02081040G2081040G2081040G2" + } + } + }, + { + "type": "upstream-oauth-provider", + "id": "030C1G60R30C1G60R30C1G60R3", + "attributes": { + "issuer": null, + "human_name": "Custom OAuth Provider", + "brand_name": null, + "created_at": "1970-01-01T00:00:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/030C1G60R30C1G60R30C1G60R3" + }, + "meta": { + "page": { + "cursor": "030C1G60R30C1G60R30C1G60R3" + } + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[first]=3", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=3", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=3", + "next": "/api/admin/v1/upstream-oauth-providers?page[after]=030C1G60R30C1G60R30C1G60R3&page[first]=3" + } + } + } + } + } + } + } + }, + "/api/admin/v1/upstream-oauth-providers/{id}": { + "get": { + "tags": [ + "upstream-oauth-provider" + ], + "summary": "Get upstream OAuth provider", + "operationId": "getUpstreamOAuthProvider", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "The upstream OAuth provider", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_UpstreamOAuthProvider" + }, + "example": { + "data": { + "type": "upstream-oauth-provider", + "id": "01040G2081040G2081040G2081", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "1970-01-01T00:00:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081" + } + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081" + } + } + } + } + }, + "404": { + "description": "Provider not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } } }, "components": { @@ -3186,62 +4840,211 @@ } }, "schemas": { + "SiteConfig": { + "type": "object", + "properties": { + "server_name": { + "description": "The Matrix server name for which this instance is configured", + "type": "string" + }, + "password_login_enabled": { + "description": "Whether password login is enabled.", + "type": "boolean" + }, + "password_registration_enabled": { + "description": "Whether password registration is enabled.", + "type": "boolean" + }, + "password_registration_email_required": { + "description": "Whether a valid email address is required for password registrations.", + "type": "boolean" + }, + "registration_token_required": { + "description": "Whether registration tokens are required for password registrations.", + "type": "boolean" + }, + "email_change_allowed": { + "description": "Whether users can change their email.", + "type": "boolean" + }, + "displayname_change_allowed": { + "description": "Whether users can change their display name.", + "type": "boolean" + }, + "password_change_allowed": { + "description": "Whether users can change their password.", + "type": "boolean" + }, + "account_recovery_allowed": { + "description": "Whether users can recover their account via email.", + "type": "boolean" + }, + "account_deactivation_allowed": { + "description": "Whether users can delete their own account.", + "type": "boolean" + }, + "captcha_enabled": { + "description": "Whether CAPTCHA during registration is enabled.", + "type": "boolean" + }, + "minimum_password_complexity": { + "description": "Minimum password complexity, between 0 and 4.\n This is a score from zxcvbn.", + "type": "integer", + "format": "uint8", + "minimum": 0, + "maximum": 4 + } + }, + "required": [ + "server_name", + "password_login_enabled", + "password_registration_enabled", + "password_registration_email_required", + "registration_token_required", + "email_change_allowed", + "displayname_change_allowed", + "password_change_allowed", + "account_recovery_allowed", + "account_deactivation_allowed", + "captcha_enabled", + "minimum_password_complexity" + ] + }, + "Version": { + "type": "object", + "properties": { + "version": { + "description": "The semver version of the app", + "type": "string" + } + }, + "required": [ + "version" + ] + }, "PaginationParams": { "type": "object", "properties": { "page[before]": { "description": "Retrieve the items before the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "page[after]": { "description": "Retrieve the items after the given ID", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "page[first]": { "description": "Retrieve the first N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 }, "page[last]": { "description": "Retrieve the last N items", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 1.0, - "nullable": true + "minimum": 1 + }, + "count": { + "description": "Include the total number of items. Defaults to `true`.", + "anyOf": [ + { + "$ref": "#/components/schemas/IncludeCount" + }, + { + "type": "null" + } + ] } } }, "ULID": { "title": "ULID", "description": "A ULID as per https://github.com/ulid/spec", - "examples": [ - "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "01J41912SC8VGAQDD50F6APK91" - ], "type": "string", - "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "IncludeCount": { + "oneOf": [ + { + "description": "Include the total number of items (default)", + "type": "string", + "enum": [ + "true" + ] + }, + { + "description": "Do not include the total number of items", + "type": "string", + "enum": [ + "false" + ] + }, + { + "description": "Only include the total number of items, skip the items themselves", + "type": "string", + "enum": [ + "only" + ] + } + ] }, "CompatSessionFilter": { "type": "object", "properties": { "filter[user]": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[user-session]": { "description": "Retrieve the items started from the given browser session", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[status]": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", - "$ref": "#/components/schemas/CompatSessionStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", + "anyOf": [ + { + "$ref": "#/components/schemas/CompatSessionStatus" + }, + { + "type": "null" + } + ] } } }, @@ -3255,52 +5058,58 @@ "PaginatedResponse_for_CompatSession": { "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links", - "meta" - ], "properties": { "meta": { "description": "Response metadata", - "$ref": "#/components/schemas/PaginationMeta" + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] }, "data": { "description": "The list of resources", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/components/schemas/SingleResource_for_CompatSession" } }, "links": { "description": "Related links", - "$ref": "#/components/schemas/PaginationLinks" + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, "PaginationMeta": { "type": "object", - "required": [ - "count" - ], "properties": { "count": { "description": "The total number of results", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint", - "minimum": 0.0 + "minimum": 0 } } }, "SingleResource_for_CompatSession": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -3308,45 +5117,82 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/CompatSession" + "allOf": [ + { + "$ref": "#/components/schemas/CompatSession" + } + ] }, "links": { "description": "Related links", - "$ref": "#/components/schemas/SelfLinks" + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] } - } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] }, "CompatSession": { "description": "A compatibility session for legacy clients", "type": "object", - "required": [ - "created_at", - "device_id", - "user_id", - "user_session_id" - ], "properties": { "user_id": { "description": "The ID of the user that owns this session", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "device_id": { "description": "The Matrix device ID of this session", - "$ref": "#/components/schemas/DeviceID" + "allOf": [ + { + "$ref": "#/components/schemas/DeviceID" + } + ] }, "user_session_id": { "description": "The ID of the user session that started this session, if any", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "redirect_uri": { "description": "The redirect URI used to login in the client, if it was an SSO login", - "type": "string", - "format": "uri", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "uri" }, "created_at": { "description": "The time this session was created", @@ -3355,64 +5201,103 @@ }, "user_agent": { "description": "The user agent string that started this session, if any", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "last_active_at": { "description": "The time this session was last active", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "last_active_ip": { "description": "The last IP address recorded for this session", - "type": "string", - "format": "ip", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "ip" }, "finished_at": { "description": "The time this session was finished", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "human_name": { "description": "The user-provided name, if any", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] } - } + }, + "required": [ + "user_id", + "device_id", + "user_session_id", + "created_at" + ] }, "DeviceID": { "title": "Device ID", - "examples": [ - "AABBCCDDEE", - "FFGGHHIIJJ" - ], + "description": "A device ID as per https://matrix.org/docs/spec/client_server/r0.6.0#device-ids", "type": "string", - "pattern": "^[A-Za-z0-9._~!$&'()*+,;=:&/-]+$" + "pattern": "^[A-Za-z0-9._~!$&'()*+,;=:&/-]+$", + "example": "AABBCCDDEE" }, "SelfLinks": { "description": "Related links", "type": "object", - "required": [ - "self" - ], "properties": { "self": { "description": "The canonical link to the current resource", "type": "string" } + }, + "required": [ + "self" + ] + }, + "SingleResourceMeta": { + "description": "Metadata associated with a resource", + "type": "object", + "properties": { + "page": { + "description": "Information about the pagination of the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMetaPage" + }, + { + "type": "null" + } + ] + } } }, + "SingleResourceMetaPage": { + "description": "Pagination metadata for a resource", + "type": "object", + "properties": { + "cursor": { + "description": "The cursor of this resource in the paginated result", + "type": "string" + } + }, + "required": [ + "cursor" + ] + }, "PaginationLinks": { "description": "Related links", "type": "object", - "required": [ - "first", - "last", - "self" - ], "properties": { "self": { "description": "The canonical link to the current page", @@ -3420,30 +5305,40 @@ }, "first": { "description": "The link to the first page of results", - "type": "string" + "type": [ + "string", + "null" + ] }, "last": { "description": "The link to the last page of results", - "type": "string" + "type": [ + "string", + "null" + ] }, "next": { - "description": "The link to the next page of results\n\nOnly present if there is a next page", - "type": "string", - "nullable": true + "description": "The link to the next page of results\n\n Only present if there is a next page", + "type": [ + "string", + "null" + ] }, "prev": { - "description": "The link to the previous page of results\n\nOnly present if there is a previous page", - "type": "string", - "nullable": true + "description": "The link to the previous page of results\n\n Only present if there is a previous page", + "type": [ + "string", + "null" + ] } - } + }, + "required": [ + "self" + ] }, "ErrorResponse": { "description": "A top-level response with a list of errors", "type": "object", - "required": [ - "errors" - ], "properties": { "errors": { "description": "The list of errors", @@ -3452,40 +5347,43 @@ "$ref": "#/components/schemas/Error" } } - } + }, + "required": [ + "errors" + ] }, "Error": { "description": "A single error", "type": "object", - "required": [ - "title" - ], "properties": { "title": { "description": "A human-readable title for the error", "type": "string" } - } + }, + "required": [ + "title" + ] }, "UlidInPath": { "type": "object", - "required": [ - "id" - ], "properties": { "id": { "title": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] } - } + }, + "required": [ + "id" + ] }, "SingleResponse_for_CompatSession": { "description": "A top-level response with a single resource", "type": "object", - "required": [ - "data", - "links" - ], "properties": { "data": { "$ref": "#/components/schemas/SingleResource_for_CompatSession" @@ -3493,43 +5391,77 @@ "links": { "$ref": "#/components/schemas/SelfLinks" } - } + }, + "required": [ + "data", + "links" + ] }, "OAuth2SessionFilter": { "type": "object", "properties": { "filter[user]": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[client]": { "description": "Retrieve the items for the given client", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[client-kind]": { "description": "Retrieve the items only for a specific client kind", - "$ref": "#/components/schemas/OAuth2ClientKind", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/OAuth2ClientKind" + }, + { + "type": "null" + } + ] }, "filter[user-session]": { "description": "Retrieve the items started from the given browser session", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[scope]": { "description": "Retrieve the items with the given scope", - "default": [], "type": "array", "items": { "type": "string" - } + }, + "default": [] }, "filter[status]": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", - "$ref": "#/components/schemas/OAuth2SessionStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", + "anyOf": [ + { + "$ref": "#/components/schemas/OAuth2SessionStatus" + }, + { + "type": "null" + } + ] } } }, @@ -3550,38 +5482,44 @@ "PaginatedResponse_for_OAuth2Session": { "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links", - "meta" - ], "properties": { "meta": { "description": "Response metadata", - "$ref": "#/components/schemas/PaginationMeta" + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] }, "data": { "description": "The list of resources", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/components/schemas/SingleResource_for_OAuth2Session" } }, "links": { "description": "Related links", - "$ref": "#/components/schemas/PaginationLinks" + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, "SingleResource_for_OAuth2Session": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -3589,26 +5527,50 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/OAuth2Session" + "allOf": [ + { + "$ref": "#/components/schemas/OAuth2Session" + } + ] }, "links": { "description": "Related links", - "$ref": "#/components/schemas/SelfLinks" + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] } - } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] }, "OAuth2Session": { "description": "A OAuth 2.0 session", "type": "object", - "required": [ - "client_id", - "created_at", - "scope" - ], "properties": { "created_at": { "description": "When the object was created", @@ -3617,23 +5579,41 @@ }, "finished_at": { "description": "When the session was finished", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "user_id": { "description": "The ID of the user who owns the session", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "user_session_id": { "description": "The ID of the browser session which started this session", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "client_id": { "description": "The ID of the client which requested this session", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "scope": { "description": "The scope granted for this session", @@ -3641,35 +5621,44 @@ }, "user_agent": { "description": "The user agent string of the client which started this session", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "last_active_at": { "description": "The last time the session was active", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "last_active_ip": { "description": "The last IP address used by the session", - "type": "string", - "format": "ip", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "ip" }, "human_name": { "description": "The user-provided name, if any", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] } - } + }, + "required": [ + "created_at", + "client_id", + "scope" + ] }, "SingleResponse_for_OAuth2Session": { "description": "A top-level response with a single resource", "type": "object", - "required": [ - "data", - "links" - ], "properties": { "data": { "$ref": "#/components/schemas/SingleResource_for_OAuth2Session" @@ -3677,51 +5666,140 @@ "links": { "$ref": "#/components/schemas/SelfLinks" } - } - }, - "SetPolicyDataRequest": { - "title": "JSON payload for the `POST /api/admin/v1/policy-data`", - "type": "object", + }, "required": [ - "data" - ], + "data", + "links" + ] + }, + "PersonalSessionFilter": { + "type": "object", "properties": { - "data": { - "examples": [ + "filter[owner_user]": { + "description": "Filter by owner user ID", + "anyOf": [ { - "hello": "world", - "foo": 42, - "bar": true + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" } ] + }, + "filter[owner_client]": { + "description": "Filter by owner `OAuth2` client ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "filter[actor_user]": { + "description": "Filter by actor user ID", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "filter[scope]": { + "description": "Retrieve the items with the given scope", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "filter[status]": { + "description": "Filter by session status", + "anyOf": [ + { + "$ref": "#/components/schemas/PersonalSessionStatus" + }, + { + "type": "null" + } + ] + }, + "filter[expires_before]": { + "description": "Filter by access token expiry date", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "filter[expires_after]": { + "description": "Filter by access token expiry date", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "filter[expires]": { + "description": "Filter by whether the access token has an expiry time", + "type": [ + "boolean", + "null" + ] } } }, - "SingleResponse_for_PolicyData": { - "description": "A top-level response with a single resource", + "PersonalSessionStatus": { + "type": "string", + "enum": [ + "active", + "revoked" + ] + }, + "PaginatedResponse_for_PersonalSession": { + "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links" - ], "properties": { + "meta": { + "description": "Response metadata", + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] + }, "data": { - "$ref": "#/components/schemas/SingleResource_for_PolicyData" + "description": "The list of resources", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SingleResource_for_PersonalSession" + } }, "links": { - "$ref": "#/components/schemas/SelfLinks" + "description": "Related links", + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, - "SingleResource_for_PolicyData": { + "SingleResource_for_PersonalSession": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -3729,25 +5807,294 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/PolicyData" + "allOf": [ + { + "$ref": "#/components/schemas/PersonalSession" + } + ] }, "links": { "description": "Related links", + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] + }, + "PersonalSession": { + "description": "A personal session (session using personal access tokens)", + "type": "object", + "properties": { + "created_at": { + "description": "When the session was created", + "type": "string", + "format": "date-time" + }, + "revoked_at": { + "description": "When the session was revoked, if applicable", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "owner_user_id": { + "description": "The ID of the user who owns this session (if user-owned)", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "owner_client_id": { + "description": "The ID of the `OAuth2` client that owns this session (if client-owned)", + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] + }, + "actor_user_id": { + "description": "The ID of the user that the session acts on behalf of", + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] + }, + "human_name": { + "description": "Human-readable name for the session", + "type": "string" + }, + "scope": { + "description": "`OAuth2` scopes for this session", + "type": "string" + }, + "last_active_at": { + "description": "When the session was last active", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "last_active_ip": { + "description": "IP address of last activity", + "type": [ + "string", + "null" + ], + "format": "ip" + }, + "expires_at": { + "description": "When the current token for this session expires.\n The session will need to be regenerated, producing a new access token,\n after this time.\n None if the current token won't expire or if the session is revoked.", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "access_token": { + "description": "The actual access token (only returned on creation)", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "created_at", + "actor_user_id", + "human_name", + "scope" + ] + }, + "CreatePersonalSessionRequest": { + "title": "JSON payload for the `POST /api/admin/v1/personal-sessions` endpoint", + "type": "object", + "properties": { + "actor_user_id": { + "description": "The user this session will act on behalf of", + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] + }, + "human_name": { + "description": "Human-readable name for the session", + "type": "string" + }, + "scope": { + "description": "`OAuth2` scopes for this session", + "type": "string" + }, + "expires_in": { + "description": "Token expiry time in seconds.\n If not set, the token won't expire.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "actor_user_id", + "human_name", + "scope" + ] + }, + "SingleResponse_for_PersonalSession": { + "description": "A top-level response with a single resource", + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SingleResource_for_PersonalSession" + }, + "links": { "$ref": "#/components/schemas/SelfLinks" } + }, + "required": [ + "data", + "links" + ] + }, + "RegeneratePersonalSessionRequest": { + "title": "JSON payload for the `POST /api/admin/v1/personal-sessions/{id}/regenerate` endpoint", + "type": "object", + "properties": { + "expires_in": { + "description": "Token expiry time in seconds.\n If not set, the token won't expire.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + } } }, + "SetPolicyDataRequest": { + "title": "JSON payload for the `POST /api/admin/v1/policy-data`", + "type": "object", + "properties": { + "data": { + "example": { + "hello": "world", + "foo": 42, + "bar": true + } + } + }, + "required": [ + "data" + ] + }, + "SingleResponse_for_PolicyData": { + "description": "A top-level response with a single resource", + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SingleResource_for_PolicyData" + }, + "links": { + "$ref": "#/components/schemas/SelfLinks" + } + }, + "required": [ + "data", + "links" + ] + }, + "SingleResource_for_PolicyData": { + "description": "A single resource, with its type, ID, attributes and related links", + "type": "object", + "properties": { + "type": { + "description": "The type of the resource", + "type": "string" + }, + "id": { + "description": "The ID of the resource", + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] + }, + "attributes": { + "description": "The attributes of the resource", + "allOf": [ + { + "$ref": "#/components/schemas/PolicyData" + } + ] + }, + "links": { + "description": "Related links", + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] + }, "PolicyData": { "description": "The policy data", "type": "object", - "required": [ - "created_at", - "data" - ], "properties": { "created_at": { "description": "The creation date of the policy data", @@ -3757,20 +6104,46 @@ "data": { "description": "The policy data content" } - } + }, + "required": [ + "created_at", + "data" + ] }, "UserFilter": { "type": "object", "properties": { "filter[admin]": { "description": "Retrieve users with (or without) the `admin` flag set", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] + }, + "filter[legacy-guest]": { + "description": "Retrieve users with (or without) the `legacy_guest` flag set", + "type": [ + "boolean", + "null" + ] + }, + "filter[search]": { + "description": "Retrieve users where the username matches contains the given string\n\n Note that this doesn't change the ordering of the result, which are\n still ordered by ID.", + "type": [ + "string", + "null" + ] }, "filter[status]": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all users, including locked ones.\n\n* `active`: Only retrieve active users\n\n* `locked`: Only retrieve locked users (includes deactivated users)\n\n* `deactivated`: Only retrieve deactivated users", - "$ref": "#/components/schemas/UserStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all users, including locked ones.\n\n * `active`: Only retrieve active users\n\n * `locked`: Only retrieve locked users (includes deactivated users)\n\n * `deactivated`: Only retrieve deactivated users", + "anyOf": [ + { + "$ref": "#/components/schemas/UserStatus" + }, + { + "type": "null" + } + ] } } }, @@ -3785,38 +6158,44 @@ "PaginatedResponse_for_User": { "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links", - "meta" - ], "properties": { "meta": { "description": "Response metadata", - "$ref": "#/components/schemas/PaginationMeta" + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] }, "data": { "description": "The list of resources", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/components/schemas/SingleResource_for_User" } }, "links": { "description": "Related links", - "$ref": "#/components/schemas/PaginationLinks" + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, "SingleResource_for_User": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -3824,26 +6203,50 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/User" + "allOf": [ + { + "$ref": "#/components/schemas/User" + } + ] }, "links": { "description": "Related links", - "$ref": "#/components/schemas/SelfLinks" + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] } - } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] }, "User": { "description": "A user", "type": "object", - "required": [ - "admin", - "created_at", - "username" - ], "properties": { "username": { "description": "The username (localpart) of the user", @@ -3856,47 +6259,57 @@ }, "locked_at": { "description": "When the user was locked. If null, the user is not locked.", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "deactivated_at": { "description": "When the user was deactivated. If null, the user is not deactivated.", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "admin": { "description": "Whether the user can request admin privileges.", "type": "boolean" + }, + "legacy_guest": { + "description": "Whether the user was a guest before migrating to MAS,", + "type": "boolean" } - } + }, + "required": [ + "username", + "created_at", + "admin", + "legacy_guest" + ] }, "AddUserRequest": { "title": "JSON payload for the `POST /api/admin/v1/users` endpoint", "type": "object", - "required": [ - "username" - ], "properties": { "username": { "description": "The username of the user to add.", "type": "string" }, "skip_homeserver_check": { - "description": "Skip checking with the homeserver whether the username is available.\n\nUse this with caution! The main reason to use this, is when a user used by an application service needs to exist in MAS to craft special tokens (like with admin access) for them", - "default": false, - "type": "boolean" + "description": "Skip checking with the homeserver whether the username is available.\n\n Use this with caution! The main reason to use this, is when a user used\n by an application service needs to exist in MAS to craft special\n tokens (like with admin access) for them", + "type": "boolean", + "default": false } - } + }, + "required": [ + "username" + ] }, "SingleResponse_for_User": { "description": "A top-level response with a single resource", "type": "object", - "required": [ - "data", - "links" - ], "properties": { "data": { "$ref": "#/components/schemas/SingleResource_for_User" @@ -3904,62 +6317,66 @@ "links": { "$ref": "#/components/schemas/SelfLinks" } - } + }, + "required": [ + "data", + "links" + ] }, "SetUserPasswordRequest": { "title": "JSON payload for the `POST /api/admin/v1/users/:id/set-password` endpoint", "type": "object", - "required": [ - "password" - ], "properties": { "password": { "description": "The password to set for the user", - "examples": [ - "hunter2" - ], - "type": "string" + "type": "string", + "example": "hunter2" }, "skip_password_check": { "description": "Skip the password complexity check", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] } - } + }, + "required": [ + "password" + ] }, "UsernamePathParam": { "type": "object", - "required": [ - "username" - ], "properties": { "username": { "description": "The username (localpart) of the user to get", "type": "string" } - } + }, + "required": [ + "username" + ] }, "UserSetAdminRequest": { "title": "JSON payload for the `POST /api/admin/v1/users/:id/set-admin` endpoint", "type": "object", - "required": [ - "admin" - ], "properties": { "admin": { "description": "Whether the user can request admin privileges.", "type": "boolean" } - } + }, + "required": [ + "admin" + ] }, "DeactivateUserRequest": { "title": "JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint", "type": "object", "properties": { "skip_erase": { - "description": "Whether to skip requesting the homeserver to GDPR-erase the user upon deactivation.", - "default": false, - "type": "boolean" + "description": "Whether to skip requesting the homeserver to GDPR-erase the user upon\n deactivation.", + "type": "boolean", + "default": false } } }, @@ -3968,51 +6385,65 @@ "properties": { "filter[user]": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[email]": { "description": "Retrieve the user email with the given email address", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] } } }, "PaginatedResponse_for_UserEmail": { "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links", - "meta" - ], "properties": { "meta": { "description": "Response metadata", - "$ref": "#/components/schemas/PaginationMeta" + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] }, "data": { "description": "The list of resources", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/components/schemas/SingleResource_for_UserEmail" } }, "links": { "description": "Related links", - "$ref": "#/components/schemas/PaginationLinks" + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, "SingleResource_for_UserEmail": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -4020,26 +6451,50 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/UserEmail" + "allOf": [ + { + "$ref": "#/components/schemas/UserEmail" + } + ] }, "links": { "description": "Related links", - "$ref": "#/components/schemas/SelfLinks" + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] } - } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] }, "UserEmail": { "description": "An email address for a user", "type": "object", - "required": [ - "created_at", - "email", - "user_id" - ], "properties": { "created_at": { "description": "When the object was created", @@ -4048,40 +6503,49 @@ }, "user_id": { "description": "The ID of the user who owns this email address", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "email": { "description": "The email address", "type": "string" } - } + }, + "required": [ + "created_at", + "user_id", + "email" + ] }, "AddUserEmailRequest": { "title": "JSON payload for the `POST /api/admin/v1/user-emails`", "type": "object", - "required": [ - "email", - "user_id" - ], "properties": { "user_id": { "description": "The ID of the user to which the email should be added.", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "email": { "description": "The email address of the user to add.", "type": "string", "format": "email" } - } + }, + "required": [ + "user_id", + "email" + ] }, "SingleResponse_for_UserEmail": { "description": "A top-level response with a single resource", "type": "object", - "required": [ - "data", - "links" - ], "properties": { "data": { "$ref": "#/components/schemas/SingleResource_for_UserEmail" @@ -4089,20 +6553,36 @@ "links": { "$ref": "#/components/schemas/SelfLinks" } - } + }, + "required": [ + "data", + "links" + ] }, "UserSessionFilter": { "type": "object", "properties": { "filter[user]": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[status]": { - "description": "Retrieve the items with the given status\n\nDefaults to retrieve all sessions, including finished ones.\n\n* `active`: Only retrieve active sessions\n\n* `finished`: Only retrieve finished sessions", - "$ref": "#/components/schemas/UserSessionStatus", - "nullable": true + "description": "Retrieve the items with the given status\n\n Defaults to retrieve all sessions, including finished ones.\n\n * `active`: Only retrieve active sessions\n\n * `finished`: Only retrieve finished sessions", + "anyOf": [ + { + "$ref": "#/components/schemas/UserSessionStatus" + }, + { + "type": "null" + } + ] } } }, @@ -4116,38 +6596,44 @@ "PaginatedResponse_for_UserSession": { "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links", - "meta" - ], "properties": { "meta": { "description": "Response metadata", - "$ref": "#/components/schemas/PaginationMeta" + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] }, "data": { "description": "The list of resources", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/components/schemas/SingleResource_for_UserSession" } }, "links": { "description": "Related links", - "$ref": "#/components/schemas/PaginationLinks" + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, "SingleResource_for_UserSession": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -4155,25 +6641,50 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/UserSession" + "allOf": [ + { + "$ref": "#/components/schemas/UserSession" + } + ] }, "links": { "description": "Related links", - "$ref": "#/components/schemas/SelfLinks" + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] } - } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] }, "UserSession": { "description": "The browser (cookie) session for a user", "type": "object", - "required": [ - "created_at", - "user_id" - ], "properties": { "created_at": { "description": "When the object was created", @@ -4182,40 +6693,52 @@ }, "finished_at": { "description": "When the session was finished", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "user_id": { "description": "The ID of the user who owns the session", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "user_agent": { "description": "The user agent string of the client which started this session", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "last_active_at": { "description": "The last time the session was active", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "last_active_ip": { "description": "The last IP address used by the session", - "type": "string", - "format": "ip", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "ip" } - } + }, + "required": [ + "created_at", + "user_id" + ] }, "SingleResponse_for_UserSession": { "description": "A top-level response with a single resource", "type": "object", - "required": [ - "data", - "links" - ], "properties": { "data": { "$ref": "#/components/schemas/SingleResource_for_UserSession" @@ -4223,68 +6746,86 @@ "links": { "$ref": "#/components/schemas/SelfLinks" } - } + }, + "required": [ + "data", + "links" + ] }, "RegistrationTokenFilter": { "type": "object", "properties": { "filter[used]": { "description": "Retrieve tokens that have (or have not) been used at least once", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] }, "filter[revoked]": { "description": "Retrieve tokens that are (or are not) revoked", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] }, "filter[expired]": { "description": "Retrieve tokens that are (or are not) expired", - "type": "boolean", - "nullable": true + "type": [ + "boolean", + "null" + ] }, "filter[valid]": { - "description": "Retrieve tokens that are (or are not) valid\n\nValid means that the token has not expired, is not revoked, and has not reached its usage limit.", - "type": "boolean", - "nullable": true + "description": "Retrieve tokens that are (or are not) valid\n\n Valid means that the token has not expired, is not revoked, and has not\n reached its usage limit.", + "type": [ + "boolean", + "null" + ] } } }, "PaginatedResponse_for_UserRegistrationToken": { "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links", - "meta" - ], "properties": { "meta": { "description": "Response metadata", - "$ref": "#/components/schemas/PaginationMeta" + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] }, "data": { "description": "The list of resources", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/components/schemas/SingleResource_for_UserRegistrationToken" } }, "links": { "description": "Related links", - "$ref": "#/components/schemas/PaginationLinks" + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, "SingleResource_for_UserRegistrationToken": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -4292,27 +6833,50 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/UserRegistrationToken" + "allOf": [ + { + "$ref": "#/components/schemas/UserRegistrationToken" + } + ] }, "links": { "description": "Related links", - "$ref": "#/components/schemas/SelfLinks" + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] } - } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] }, "UserRegistrationToken": { "description": "A registration token", "type": "object", - "required": [ - "created_at", - "times_used", - "token", - "valid" - ], "properties": { "token": { "description": "The token string", @@ -4324,16 +6888,18 @@ }, "usage_limit": { "description": "Maximum number of times this token can be used", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint32", - "minimum": 0.0, - "nullable": true + "minimum": 0 }, "times_used": { "description": "Number of times this token has been used", "type": "integer", "format": "uint32", - "minimum": 0.0 + "minimum": 0 }, "created_at": { "description": "When the token was created", @@ -4342,23 +6908,35 @@ }, "last_used_at": { "description": "When the token was last used. If null, the token has never been used.", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "expires_at": { "description": "When the token expires. If null, the token never expires.", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "revoked_at": { "description": "When the token was revoked. If null, the token is not revoked.", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" } - } + }, + "required": [ + "token", + "valid", + "times_used", + "created_at" + ] }, "AddUserRegistrationTokenRequest": { "title": "JSON payload for the `POST /api/admin/v1/user-registration-tokens`", @@ -4366,31 +6944,33 @@ "properties": { "token": { "description": "The token string. If not provided, a random token will be generated.", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] }, "usage_limit": { - "description": "Maximum number of times this token can be used. If not provided, the token can be used an unlimited number of times.", - "type": "integer", + "description": "Maximum number of times this token can be used. If not provided, the\n token can be used an unlimited number of times.", + "type": [ + "integer", + "null" + ], "format": "uint32", - "minimum": 0.0, - "nullable": true + "minimum": 0 }, "expires_at": { "description": "When the token expires. If not provided, the token never expires.", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" } } }, "SingleResponse_for_UserRegistrationToken": { "description": "A top-level response with a single resource", "type": "object", - "required": [ - "data", - "links" - ], "properties": { "data": { "$ref": "#/components/schemas/SingleResource_for_UserRegistrationToken" @@ -4398,7 +6978,11 @@ "links": { "$ref": "#/components/schemas/SelfLinks" } - } + }, + "required": [ + "data", + "links" + ] }, "EditUserRegistrationTokenRequest": { "title": "JSON payload for the `PUT /api/admin/v1/user-registration-tokens/{id}` endpoint", @@ -4406,16 +6990,20 @@ "properties": { "expires_at": { "description": "New expiration date for the token, or null to remove expiration", - "type": "string", - "format": "date-time", - "nullable": true + "type": [ + "string", + "null" + ], + "format": "date-time" }, "usage_limit": { "description": "New usage limit for the token, or null to remove the limit", - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint32", - "minimum": 0.0, - "nullable": true + "minimum": 0 } } }, @@ -4424,56 +7012,76 @@ "properties": { "filter[user]": { "description": "Retrieve the items for the given user", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[provider]": { "description": "Retrieve the items for the given provider", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "filter[subject]": { "description": "Retrieve the items with the given subject", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] } } }, "PaginatedResponse_for_UpstreamOAuthLink": { "description": "A top-level response with a page of resources", "type": "object", - "required": [ - "data", - "links", - "meta" - ], "properties": { "meta": { "description": "Response metadata", - "$ref": "#/components/schemas/PaginationMeta" + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] }, "data": { "description": "The list of resources", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthLink" } }, "links": { "description": "Related links", - "$ref": "#/components/schemas/PaginationLinks" + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] } - } + }, + "required": [ + "links" + ] }, "SingleResource_for_UpstreamOAuthLink": { "description": "A single resource, with its type, ID, attributes and related links", "type": "object", - "required": [ - "attributes", - "id", - "links", - "type" - ], "properties": { "type": { "description": "The type of the resource", @@ -4481,26 +7089,50 @@ }, "id": { "description": "The ID of the resource", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "attributes": { "description": "The attributes of the resource", - "$ref": "#/components/schemas/UpstreamOAuthLink" + "allOf": [ + { + "$ref": "#/components/schemas/UpstreamOAuthLink" + } + ] }, "links": { "description": "Related links", - "$ref": "#/components/schemas/SelfLinks" + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] } - } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] }, "UpstreamOAuthLink": { "description": "An upstream OAuth 2.0 link", "type": "object", - "required": [ - "created_at", - "provider_id", - "subject" - ], "properties": { "created_at": { "description": "When the object was created", @@ -4509,7 +7141,11 @@ }, "provider_id": { "description": "The ID of the provider", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "subject": { "description": "The subject of the upstream account, unique per provider", @@ -4517,32 +7153,48 @@ }, "user_id": { "description": "The ID of the user who owns this link, if any", - "$ref": "#/components/schemas/ULID", - "nullable": true + "anyOf": [ + { + "$ref": "#/components/schemas/ULID" + }, + { + "type": "null" + } + ] }, "human_account_name": { "description": "A human-readable name of the upstream account", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] } - } + }, + "required": [ + "created_at", + "provider_id", + "subject" + ] }, "AddUpstreamOauthLinkRequest": { "title": "JSON payload for the `POST /api/admin/v1/upstream-oauth-links`", "type": "object", - "required": [ - "provider_id", - "subject", - "user_id" - ], "properties": { "user_id": { "description": "The ID of the user to which the link should be added.", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "provider_id": { "description": "The ID of the upstream provider to which the link is for.", - "$ref": "#/components/schemas/ULID" + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] }, "subject": { "description": "The subject (sub) claim of the user on the provider.", @@ -4550,18 +7202,21 @@ }, "human_account_name": { "description": "A human readable account name.", - "type": "string", - "nullable": true + "type": [ + "string", + "null" + ] } - } + }, + "required": [ + "user_id", + "provider_id", + "subject" + ] }, "SingleResponse_for_UpstreamOAuthLink": { "description": "A top-level response with a single resource", "type": "object", - "required": [ - "data", - "links" - ], "properties": { "data": { "$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthLink" @@ -4569,7 +7224,171 @@ "links": { "$ref": "#/components/schemas/SelfLinks" } + }, + "required": [ + "data", + "links" + ] + }, + "UpstreamOAuthProviderFilter": { + "type": "object", + "properties": { + "filter[enabled]": { + "description": "Retrieve providers that are (or are not) enabled", + "type": [ + "boolean", + "null" + ] + } } + }, + "PaginatedResponse_for_UpstreamOAuthProvider": { + "description": "A top-level response with a page of resources", + "type": "object", + "properties": { + "meta": { + "description": "Response metadata", + "anyOf": [ + { + "$ref": "#/components/schemas/PaginationMeta" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "The list of resources", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthProvider" + } + }, + "links": { + "description": "Related links", + "allOf": [ + { + "$ref": "#/components/schemas/PaginationLinks" + } + ] + } + }, + "required": [ + "links" + ] + }, + "SingleResource_for_UpstreamOAuthProvider": { + "description": "A single resource, with its type, ID, attributes and related links", + "type": "object", + "properties": { + "type": { + "description": "The type of the resource", + "type": "string" + }, + "id": { + "description": "The ID of the resource", + "allOf": [ + { + "$ref": "#/components/schemas/ULID" + } + ] + }, + "attributes": { + "description": "The attributes of the resource", + "allOf": [ + { + "$ref": "#/components/schemas/UpstreamOAuthProvider" + } + ] + }, + "links": { + "description": "Related links", + "allOf": [ + { + "$ref": "#/components/schemas/SelfLinks" + } + ] + }, + "meta": { + "description": "Metadata about the resource", + "anyOf": [ + { + "$ref": "#/components/schemas/SingleResourceMeta" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "type", + "id", + "attributes", + "links" + ] + }, + "UpstreamOAuthProvider": { + "description": "An upstream OAuth 2.0 provider", + "type": "object", + "properties": { + "issuer": { + "description": "The OIDC issuer of the provider", + "type": [ + "string", + "null" + ] + }, + "human_name": { + "description": "A human-readable name for the provider", + "type": [ + "string", + "null" + ] + }, + "brand_name": { + "description": "A brand identifier, e.g. \"apple\" or \"google\"", + "type": [ + "string", + "null" + ] + }, + "created_at": { + "description": "When the provider was created", + "type": "string", + "format": "date-time" + }, + "disabled_at": { + "description": "When the provider was disabled. If null, the provider is enabled.", + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + "required": [ + "created_at" + ] + }, + "SingleResponse_for_UpstreamOAuthProvider": { + "description": "A top-level response with a single resource", + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthProvider" + }, + "links": { + "$ref": "#/components/schemas/SelfLinks" + } + }, + "required": [ + "data", + "links" + ] } } }, @@ -4586,6 +7405,10 @@ } ], "tags": [ + { + "name": "server", + "description": "Information about the server" + }, { "name": "compat-session", "description": "Manage compatibility sessions from legacy clients" @@ -4617,6 +7440,10 @@ { "name": "upstream-oauth-link", "description": "Manage links between local users and identities from upstream OAuth 2.0 providers" + }, + { + "name": "upstream-oauth-provider", + "description": "Manage upstream OAuth 2.0 providers" } ] } diff --git a/docs/config.schema.json b/docs/config.schema.json index 8a97cad13..d38ff0f2d 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -3,10 +3,6 @@ "title": "RootConfig", "description": "Application configuration root", "type": "object", - "required": [ - "matrix", - "secrets" - ], "properties": { "clients": { "description": "List of OAuth 2.0/OIDC clients config", @@ -169,7 +165,7 @@ ] }, "rate_limiting": { - "description": "Configuration related to limiting the rate of user actions to prevent abuse", + "description": "Configuration related to limiting the rate of user actions to prevent\n abuse", "allOf": [ { "$ref": "#/definitions/RateLimitingConfig" @@ -201,7 +197,7 @@ ] }, "account": { - "description": "Configuration section to configure features related to account management", + "description": "Configuration section to configure features related to account\n management", "allOf": [ { "$ref": "#/definitions/AccountConfig" @@ -217,14 +213,14 @@ ] } }, + "required": [ + "secrets", + "matrix" + ], "definitions": { "ClientConfig": { "description": "An OAuth 2.0 client configuration", "type": "object", - "required": [ - "client_auth_method", - "client_id" - ], "properties": { "client_id": { "description": "A ULID as per https://github.com/ulid/spec", @@ -241,19 +237,42 @@ }, "client_name": { "description": "Name of the `OAuth2` client", - "type": "string" + "type": [ + "string", + "null" + ] + }, + "client_secret_file": { + "description": "Path to the file containing the client secret. The client secret is used\n by the `client_secret_basic`, `client_secret_post` and\n `client_secret_jwt` authentication methods.", + "type": [ + "string", + "null" + ] + }, + "client_secret": { + "description": "Alternative to `client_secret_file`: Reads the client secret directly\n from the config.", + "type": [ + "string", + "null" + ] }, "jwks": { - "description": "The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication method. Mutually exclusive with `jwks_uri`", - "allOf": [ + "description": "The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication\n method. Mutually exclusive with `jwks_uri`", + "anyOf": [ { "$ref": "#/definitions/JsonWebKeySet_for_JsonWebKeyPublicParameters" + }, + { + "type": "null" } ] }, "jwks_uri": { - "description": "The URL of the JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication method. Mutually exclusive with `jwks`", - "type": "string", + "description": "The URL of the JSON Web Key Set (JWKS) used by the `private_key_jwt`\n authentication method. Mutually exclusive with `jwks`", + "type": [ + "string", + "null" + ], "format": "uri" }, "redirect_uris": { @@ -263,16 +282,12 @@ "type": "string", "format": "uri" } - }, - "client_secret_file": { - "description": "Path to the file containing the client secret. The client secret is used by the `client_secret_basic`, `client_secret_post` and `client_secret_jwt` authentication methods.", - "type": "string" - }, - "client_secret": { - "description": "Alternative to `client_secret_file`: Reads the client secret directly from the config.", - "type": "string" } - } + }, + "required": [ + "client_id", + "client_auth_method" + ] }, "ClientAuthMethodConfig": { "description": "Authentication method used by clients", @@ -280,45 +295,32 @@ { "description": "`none`: No authentication", "type": "string", - "enum": [ - "none" - ] + "const": "none" }, { - "description": "`client_secret_basic`: `client_id` and `client_secret` used as basic authorization credentials", + "description": "`client_secret_basic`: `client_id` and `client_secret` used as basic\n authorization credentials", "type": "string", - "enum": [ - "client_secret_basic" - ] + "const": "client_secret_basic" }, { - "description": "`client_secret_post`: `client_id` and `client_secret` sent in the request body", + "description": "`client_secret_post`: `client_id` and `client_secret` sent in the\n request body", "type": "string", - "enum": [ - "client_secret_post" - ] + "const": "client_secret_post" }, { - "description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed using the `client_secret`", + "description": "`client_secret_basic`: a `client_assertion` sent in the request body and\n signed using the `client_secret`", "type": "string", - "enum": [ - "client_secret_jwt" - ] + "const": "client_secret_jwt" }, { - "description": "`client_secret_basic`: a `client_assertion` sent in the request body and signed by an asymmetric key", + "description": "`client_secret_basic`: a `client_assertion` sent in the request body and\n signed by an asymmetric key", "type": "string", - "enum": [ - "private_key_jwt" - ] + "const": "private_key_jwt" } ] }, "JsonWebKeySet_for_JsonWebKeyPublicParameters": { "type": "object", - "required": [ - "keys" - ], "properties": { "keys": { "type": "array", @@ -326,100 +328,54 @@ "$ref": "#/definitions/JsonWebKey_for_JsonWebKeyPublicParameters" } } - } + }, + "required": [ + "keys" + ] }, "JsonWebKey_for_JsonWebKeyPublicParameters": { "type": "object", - "oneOf": [ - { - "type": "object", - "required": [ - "e", - "kty", - "n" - ], - "properties": { - "kty": { - "type": "string", - "enum": [ - "RSA" - ] - }, - "n": { - "type": "string" - }, - "e": { - "type": "string" - } - } - }, - { - "type": "object", - "required": [ - "crv", - "kty", - "x", - "y" - ], - "properties": { - "kty": { - "type": "string", - "enum": [ - "EC" - ] - }, - "crv": { - "$ref": "#/definitions/JsonWebKeyEcEllipticCurve" - }, - "x": { - "type": "string" - }, - "y": { - "type": "string" - } - } - }, - { - "type": "object", - "required": [ - "crv", - "kty", - "x" - ], - "properties": { - "kty": { - "type": "string", - "enum": [ - "OKP" - ] - }, - "crv": { - "$ref": "#/definitions/JsonWebKeyOkpEllipticCurve" - }, - "x": { - "type": "string" - } - } - } - ], "properties": { "use": { - "$ref": "#/definitions/JsonWebKeyUse" + "anyOf": [ + { + "$ref": "#/definitions/JsonWebKeyUse" + }, + { + "type": "null" + } + ] }, "key_ops": { - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/definitions/JsonWebKeyOperation" } }, "alg": { - "$ref": "#/definitions/JsonWebSignatureAlg" + "anyOf": [ + { + "$ref": "#/definitions/JsonWebSignatureAlg" + }, + { + "type": "null" + } + ] }, "kid": { - "type": "string" + "type": [ + "string", + "null" + ] }, "x5u": { - "type": "string" + "type": [ + "string", + "null" + ] }, "x5c": { "type": "array", @@ -428,12 +384,162 @@ } }, "x5t": { - "type": "string" + "type": [ + "string", + "null" + ] }, "x5t#S256": { + "type": [ + "string", + "null" + ] + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "kty": { + "type": "string", + "const": "RSA" + } + }, + "required": [ + "kty" + ], + "allOf": [ + { + "$ref": "#/definitions/RsaPublicParameters" + } + ] + }, + { + "type": "object", + "properties": { + "kty": { + "type": "string", + "const": "EC" + } + }, + "required": [ + "kty" + ], + "allOf": [ + { + "$ref": "#/definitions/EcPublicParameters" + } + ] + }, + { + "type": "object", + "properties": { + "kty": { + "type": "string", + "const": "OKP" + } + }, + "required": [ + "kty" + ], + "allOf": [ + { + "$ref": "#/definitions/OkpPublicParameters" + } + ] + } + ] + }, + "RsaPublicParameters": { + "type": "object", + "properties": { + "n": { + "type": "string" + }, + "e": { "type": "string" } - } + }, + "required": [ + "n", + "e" + ] + }, + "JsonWebKeyEcEllipticCurve": { + "description": "JSON Web Key EC Elliptic Curve", + "anyOf": [ + { + "description": "P-256 Curve", + "const": "P-256" + }, + { + "description": "P-384 Curve", + "const": "P-384" + }, + { + "description": "P-521 Curve", + "const": "P-521" + }, + { + "description": "SECG secp256k1 curve", + "const": "secp256k1" + } + ] + }, + "EcPublicParameters": { + "type": "object", + "properties": { + "crv": { + "$ref": "#/definitions/JsonWebKeyEcEllipticCurve" + }, + "x": { + "type": "string" + }, + "y": { + "type": "string" + } + }, + "required": [ + "crv", + "x", + "y" + ] + }, + "JsonWebKeyOkpEllipticCurve": { + "description": "JSON Web Key OKP Elliptic Curve", + "anyOf": [ + { + "description": "Ed25519 signature algorithm key pairs", + "const": "Ed25519" + }, + { + "description": "Ed448 signature algorithm key pairs", + "const": "Ed448" + }, + { + "description": "X25519 function key pairs", + "const": "X25519" + }, + { + "description": "X448 function key pairs", + "const": "X448" + } + ] + }, + "OkpPublicParameters": { + "type": "object", + "properties": { + "crv": { + "$ref": "#/definitions/JsonWebKeyOkpEllipticCurve" + }, + "x": { + "type": "string" + } + }, + "required": [ + "crv", + "x" + ] }, "JsonWebKeyUse": { "description": "JSON Web Key Use", @@ -558,65 +664,25 @@ } ] }, - "JsonWebKeyEcEllipticCurve": { - "description": "JSON Web Key EC Elliptic Curve", - "anyOf": [ - { - "description": "P-256 Curve", - "const": "P-256" - }, - { - "description": "P-384 Curve", - "const": "P-384" - }, - { - "description": "P-521 Curve", - "const": "P-521" - }, - { - "description": "SECG secp256k1 curve", - "const": "secp256k1" - } - ] - }, - "JsonWebKeyOkpEllipticCurve": { - "description": "JSON Web Key OKP Elliptic Curve", - "anyOf": [ - { - "description": "Ed25519 signature algorithm key pairs", - "const": "Ed25519" - }, - { - "description": "Ed448 signature algorithm key pairs", - "const": "Ed448" - }, - { - "description": "X25519 function key pairs", - "const": "X25519" - }, - { - "description": "X448 function key pairs", - "const": "X448" - } - ] - }, "HttpConfig": { "description": "Configuration related to the web server", "type": "object", - "required": [ - "public_base" - ], "properties": { "listeners": { "description": "List of listeners to run", - "default": [], "type": "array", "items": { "$ref": "#/definitions/ListenerConfig" - } + }, + "default": [] }, "trusted_proxies": { - "description": "List of trusted reverse proxies that can set the `X-Forwarded-For` header", + "description": "List of trusted reverse proxies that can set the `X-Forwarded-For`\n header", + "type": "array", + "items": { + "type": "string", + "format": "ip" + }, "default": [ "192.168.0.0/16", "172.16.0.0/12", @@ -624,11 +690,7 @@ "127.0.0.1/8", "fd00::/8", "::1/128" - ], - "type": "array", - "items": { - "$ref": "#/definitions/IpNetwork" - } + ] }, "public_base": { "description": "Public URL base from where the authentication service is reachable", @@ -637,22 +699,27 @@ }, "issuer": { "description": "OIDC issuer URL. Defaults to `public_base` if not set.", - "type": "string", + "type": [ + "string", + "null" + ], "format": "uri" } - } + }, + "required": [ + "public_base" + ] }, "ListenerConfig": { "description": "Configuration of a listener", "type": "object", - "required": [ - "binds", - "resources" - ], "properties": { "name": { - "description": "A unique name for this listener which will be shown in traces and in metrics labels", - "type": "string" + "description": "A unique name for this listener which will be shown in traces and in\n metrics labels", + "type": [ + "string", + "null" + ] }, "resources": { "description": "List of resources to mount", @@ -663,7 +730,10 @@ }, "prefix": { "description": "HTTP prefix to mount the resources on", - "type": "string" + "type": [ + "string", + "null" + ] }, "binds": { "description": "List of sockets to bind", @@ -674,18 +744,25 @@ }, "proxy_protocol": { "description": "Accept `HAProxy`'s Proxy Protocol V1", - "default": false, - "type": "boolean" + "type": "boolean", + "default": false }, "tls": { "description": "If set, makes the listener use TLS with the provided certificate and key", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/TlsConfig" + }, + { + "type": "null" } ] } - } + }, + "required": [ + "resources", + "binds" + ] }, "Resource": { "description": "HTTP resources to mount", @@ -693,76 +770,59 @@ { "description": "Healthcheck endpoint (/health)", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "health" - ] + "const": "health" } - } + }, + "required": [ + "name" + ] }, { "description": "Prometheus metrics endpoint (/metrics)", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "prometheus" - ] + "const": "prometheus" } - } + }, + "required": [ + "name" + ] }, { "description": "OIDC discovery endpoints", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "discovery" - ] + "const": "discovery" } - } + }, + "required": [ + "name" + ] }, { "description": "Pages destined to be viewed by humans", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "human" - ] + "const": "human" } - } + }, + "required": [ + "name" + ] }, { "description": "GraphQL endpoint", "type": "object", - "required": [ - "name" - ], "properties": { - "name": { - "type": "string", - "enum": [ - "graphql" - ] - }, "playground": { "description": "Enabled the GraphQL playground", "type": "boolean" @@ -770,87 +830,84 @@ "undocumented_oauth2_access": { "description": "Allow access for OAuth 2.0 clients (undocumented)", "type": "boolean" + }, + "name": { + "type": "string", + "const": "graphql" } - } + }, + "required": [ + "name" + ] }, { "description": "OAuth-related APIs", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "oauth" - ] + "const": "oauth" } - } + }, + "required": [ + "name" + ] }, { "description": "Matrix compatibility API", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "compat" - ] + "const": "compat" } - } + }, + "required": [ + "name" + ] }, { "description": "Static files", "type": "object", - "required": [ - "name" - ], "properties": { - "name": { - "type": "string", - "enum": [ - "assets" - ] - }, "path": { "description": "Path to the directory to serve.", "type": "string" + }, + "name": { + "type": "string", + "const": "assets" } - } + }, + "required": [ + "name" + ] }, { "description": "Admin API, served at `/api/admin/v1`", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "adminapi" - ] + "const": "adminapi" } - } + }, + "required": [ + "name" + ] }, { - "description": "Mount a \"/connection-info\" handler which helps debugging informations on the upstream connection", + "description": "Mount a \"/connection-info\" handler which helps debugging informations on\n the upstream connection", "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string", - "enum": [ - "connection-info" - ] + "const": "connection-info" } - } + }, + "required": [ + "name" + ] } ] }, @@ -860,67 +917,71 @@ { "description": "Listen on the specified host and port", "type": "object", - "required": [ - "port" - ], "properties": { "host": { - "description": "Host on which to listen.\n\nDefaults to listening on all addresses", - "type": "string" + "description": "Host on which to listen.\n\n Defaults to listening on all addresses", + "type": [ + "string", + "null" + ] }, "port": { "description": "Port on which to listen.", "type": "integer", "format": "uint16", - "minimum": 0.0 + "minimum": 0, + "maximum": 65535 } - } + }, + "required": [ + "port" + ] }, { "description": "Listen on the specified address", "type": "object", - "required": [ - "address" - ], "properties": { "address": { "description": "Host and port on which to listen", + "type": "string", "examples": [ "[::1]:8080", "[::]:8080", "127.0.0.1:8080", "0.0.0.0:8080" - ], - "type": "string" + ] } - } + }, + "required": [ + "address" + ] }, { "description": "Listen on a UNIX domain socket", "type": "object", - "required": [ - "socket" - ], "properties": { "socket": { "description": "Path to the socket", "type": "string" } - } + }, + "required": [ + "socket" + ] }, { - "description": "Accept connections on file descriptors passed by the parent process.\n\nThis is useful for grabbing sockets passed by systemd.\n\nSee ", + "description": "Accept connections on file descriptors passed by the parent process.\n\n This is useful for grabbing sockets passed by systemd.\n\n See ", "type": "object", "properties": { "fd": { - "description": "Index of the file descriptor. Note that this is offseted by 3 because of the standard input/output sockets, so setting here a value of `0` will grab the file descriptor `3`", - "default": 0, + "description": "Index of the file descriptor. Note that this is offseted by 3\n because of the standard input/output sockets, so setting\n here a value of `0` will grab the file descriptor `3`", "type": "integer", "format": "uint", - "minimum": 0.0 + "minimum": 0, + "default": 0 }, "kind": { - "description": "Whether the socket is a TCP socket or a UNIX domain socket. Defaults to TCP.", + "description": "Whether the socket is a TCP socket or a UNIX domain socket. Defaults\n to TCP.", "default": "tcp", "allOf": [ { @@ -938,16 +999,12 @@ { "description": "UNIX domain socket", "type": "string", - "enum": [ - "unix" - ] + "const": "unix" }, { "description": "TCP socket", "type": "string", - "enum": [ - "tcp" - ] + "const": "tcp" } ] }, @@ -956,169 +1013,201 @@ "type": "object", "properties": { "certificate": { - "description": "PEM-encoded X509 certificate chain\n\nExactly one of `certificate` or `certificate_file` must be set.", - "type": "string" + "description": "PEM-encoded X509 certificate chain\n\n Exactly one of `certificate` or `certificate_file` must be set.", + "type": [ + "string", + "null" + ] }, "certificate_file": { - "description": "File containing the PEM-encoded X509 certificate chain\n\nExactly one of `certificate` or `certificate_file` must be set.", - "type": "string" + "description": "File containing the PEM-encoded X509 certificate chain\n\n Exactly one of `certificate` or `certificate_file` must be set.", + "type": [ + "string", + "null" + ] }, "key": { - "description": "PEM-encoded private key\n\nExactly one of `key` or `key_file` must be set.", - "type": "string" + "description": "PEM-encoded private key\n\n Exactly one of `key` or `key_file` must be set.", + "type": [ + "string", + "null" + ] }, "key_file": { - "description": "File containing a PEM or DER-encoded private key\n\nExactly one of `key` or `key_file` must be set.", - "type": "string" + "description": "File containing a PEM or DER-encoded private key\n\n Exactly one of `key` or `key_file` must be set.", + "type": [ + "string", + "null" + ] }, "password": { - "description": "Password used to decode the private key\n\nOne of `password` or `password_file` must be set if the key is encrypted.", - "type": "string" + "description": "Password used to decode the private key\n\n One of `password` or `password_file` must be set if the key is\n encrypted.", + "type": [ + "string", + "null" + ] }, "password_file": { - "description": "Password file used to decode the private key\n\nOne of `password` or `password_file` must be set if the key is encrypted.", - "type": "string" + "description": "Password file used to decode the private key\n\n One of `password` or `password_file` must be set if the key is\n encrypted.", + "type": [ + "string", + "null" + ] } } }, - "IpNetwork": { - "oneOf": [ - { - "title": "v4", - "allOf": [ - { - "$ref": "#/definitions/Ipv4Network" - } - ] - }, - { - "title": "v6", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Network" - } - ] - } - ], - "x-rust-type": "ipnetwork::IpNetwork" - }, - "Ipv4Network": { - "type": "string", - "pattern": "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\/(3[0-2]|[0-2]?[0-9])$", - "x-rust-type": "ipnetwork::Ipv4Network" - }, - "Ipv6Network": { - "type": "string", - "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\")[/](12[0-8]|1[0-1][0-9]|[0-9]?[0-9])$", - "x-rust-type": "ipnetwork::Ipv6Network" - }, "DatabaseConfig": { "description": "Database connection configuration", "type": "object", "properties": { "uri": { - "description": "Connection URI\n\nThis must not be specified if `host`, `port`, `socket`, `username`, `password`, or `database` are specified.", - "default": "postgresql://", - "type": "string", - "format": "uri" + "description": "Connection URI\n\n This must not be specified if `host`, `port`, `socket`, `username`,\n `password`, or `database` are specified.", + "type": [ + "string", + "null" + ], + "format": "uri", + "default": "postgresql://" }, "host": { - "description": "Name of host to connect to\n\nThis must not be specified if `uri` is specified.", - "allOf": [ + "description": "Name of host to connect to\n\n This must not be specified if `uri` is specified.", + "anyOf": [ { "$ref": "#/definitions/Hostname" + }, + { + "type": "null" } ] }, "port": { - "description": "Port number to connect at the server host\n\nThis must not be specified if `uri` is specified.", - "type": "integer", + "description": "Port number to connect at the server host\n\n This must not be specified if `uri` is specified.", + "type": [ + "integer", + "null" + ], "format": "uint16", - "maximum": 65535.0, - "minimum": 1.0 + "minimum": 1, + "maximum": 65535 }, "socket": { - "description": "Directory containing the UNIX socket to connect to\n\nThis must not be specified if `uri` is specified.", - "type": "string" + "description": "Directory containing the UNIX socket to connect to\n\n This must not be specified if `uri` is specified.", + "type": [ + "string", + "null" + ] }, "username": { - "description": "PostgreSQL user name to connect as\n\nThis must not be specified if `uri` is specified.", - "type": "string" + "description": "PostgreSQL user name to connect as\n\n This must not be specified if `uri` is specified.", + "type": [ + "string", + "null" + ] }, "password": { - "description": "Password to be used if the server demands password authentication\n\nThis must not be specified if `uri` is specified.", - "type": "string" + "description": "Password to be used if the server demands password authentication\n\n This must not be specified if `uri` is specified.", + "type": [ + "string", + "null" + ] }, "database": { - "description": "The database name\n\nThis must not be specified if `uri` is specified.", - "type": "string" + "description": "The database name\n\n This must not be specified if `uri` is specified.", + "type": [ + "string", + "null" + ] }, "ssl_mode": { "description": "How to handle SSL connections", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/PgSslMode" + }, + { + "type": "null" } ] }, "ssl_ca": { - "description": "The PEM-encoded root certificate for SSL connections\n\nThis must not be specified if the `ssl_ca_file` option is specified.", - "type": "string" + "description": "The PEM-encoded root certificate for SSL connections\n\n This must not be specified if the `ssl_ca_file` option is specified.", + "type": [ + "string", + "null" + ] }, "ssl_ca_file": { - "description": "Path to the root certificate for SSL connections\n\nThis must not be specified if the `ssl_ca` option is specified.", - "type": "string" + "description": "Path to the root certificate for SSL connections\n\n This must not be specified if the `ssl_ca` option is specified.", + "type": [ + "string", + "null" + ] }, "ssl_certificate": { - "description": "The PEM-encoded client certificate for SSL connections\n\nThis must not be specified if the `ssl_certificate_file` option is specified.", - "type": "string" + "description": "The PEM-encoded client certificate for SSL connections\n\n This must not be specified if the `ssl_certificate_file` option is\n specified.", + "type": [ + "string", + "null" + ] }, "ssl_certificate_file": { - "description": "Path to the client certificate for SSL connections\n\nThis must not be specified if the `ssl_certificate` option is specified.", - "type": "string" + "description": "Path to the client certificate for SSL connections\n\n This must not be specified if the `ssl_certificate` option is specified.", + "type": [ + "string", + "null" + ] }, "ssl_key": { - "description": "The PEM-encoded client key for SSL connections\n\nThis must not be specified if the `ssl_key_file` option is specified.", - "type": "string" + "description": "The PEM-encoded client key for SSL connections\n\n This must not be specified if the `ssl_key_file` option is specified.", + "type": [ + "string", + "null" + ] }, "ssl_key_file": { - "description": "Path to the client key for SSL connections\n\nThis must not be specified if the `ssl_key` option is specified.", - "type": "string" + "description": "Path to the client key for SSL connections\n\n This must not be specified if the `ssl_key` option is specified.", + "type": [ + "string", + "null" + ] }, "max_connections": { "description": "Set the maximum number of connections the pool should maintain", - "default": 10, "type": "integer", "format": "uint32", - "minimum": 1.0 + "minimum": 1, + "default": 10 }, "min_connections": { "description": "Set the minimum number of connections the pool should maintain", - "default": 0, "type": "integer", "format": "uint32", - "minimum": 0.0 + "minimum": 0, + "default": 0 }, "connect_timeout": { "description": "Set the amount of time to attempt connecting to the database", - "default": 30, "type": "integer", "format": "uint64", - "minimum": 0.0 + "minimum": 0, + "default": 30 }, "idle_timeout": { "description": "Set a maximum idle duration for individual connections", - "default": 600, - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint64", - "minimum": 0.0 + "minimum": 0, + "default": 600 }, "max_lifetime": { "description": "Set the maximum lifetime of individual connections", - "default": 1800, "type": "integer", "format": "uint64", - "minimum": 0.0 + "minimum": 0, + "default": 1800 } } }, @@ -1127,49 +1216,37 @@ "format": "hostname" }, "PgSslMode": { - "description": "Options for controlling the level of protection provided for PostgreSQL SSL connections.", + "description": "Options for controlling the level of protection provided for PostgreSQL SSL\n connections.", "oneOf": [ { "description": "Only try a non-SSL connection.", "type": "string", - "enum": [ - "disable" - ] + "const": "disable" }, { "description": "First try a non-SSL connection; if that fails, try an SSL connection.", "type": "string", - "enum": [ - "allow" - ] + "const": "allow" }, { "description": "First try an SSL connection; if that fails, try a non-SSL connection.", "type": "string", - "enum": [ - "prefer" - ] + "const": "prefer" }, { - "description": "Only try an SSL connection. If a root CA file is present, verify the connection in the same way as if `VerifyCa` was specified.", + "description": "Only try an SSL connection. If a root CA file is present, verify the\n connection in the same way as if `VerifyCa` was specified.", "type": "string", - "enum": [ - "require" - ] + "const": "require" }, { - "description": "Only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA).", + "description": "Only try an SSL connection, and verify that the server certificate is\n issued by a trusted certificate authority (CA).", "type": "string", - "enum": [ - "verify-ca" - ] + "const": "verify-ca" }, { - "description": "Only try an SSL connection; verify that the server certificate is issued by a trusted CA and that the requested server host name matches that in the certificate.", + "description": "Only try an SSL connection; verify that the server certificate is issued\n by a trusted CA and that the requested server host name matches that\n in the certificate.", "type": "string", - "enum": [ - "verify-full" - ] + "const": "verify-full" } ] }, @@ -1218,27 +1295,33 @@ }, "endpoint": { "description": "OTLP exporter: OTLP over HTTP compatible endpoint", - "default": "https://localhost:4318", - "type": "string", - "format": "uri" + "type": [ + "string", + "null" + ], + "format": "uri", + "default": "https://localhost:4318" }, "propagators": { "description": "List of propagation formats to use for incoming and outgoing requests", - "default": [], "type": "array", "items": { "$ref": "#/definitions/Propagator" - } + }, + "default": [] }, "sample_rate": { - "description": "Sample rate for traces\n\nDefaults to `1.0` if not set.", + "description": "Sample rate for traces\n\n Defaults to `1.0` if not set.", + "type": [ + "number", + "null" + ], + "format": "double", "examples": [ 0.5 ], - "type": "number", - "format": "double", - "maximum": 1.0, - "minimum": 0.0 + "minimum": 0.0, + "maximum": 1.0 } } }, @@ -1248,23 +1331,17 @@ { "description": "Don't export traces", "type": "string", - "enum": [ - "none" - ] + "const": "none" }, { "description": "Export traces to the standard output. Only useful for debugging", "type": "string", - "enum": [ - "stdout" - ] + "const": "stdout" }, { "description": "Export traces to an OpenTelemetry protocol compatible endpoint", "type": "string", - "enum": [ - "otlp" - ] + "const": "otlp" } ] }, @@ -1274,23 +1351,17 @@ { "description": "Propagate according to the W3C Trace Context specification", "type": "string", - "enum": [ - "tracecontext" - ] + "const": "tracecontext" }, { "description": "Propagate according to the W3C Baggage specification", "type": "string", - "enum": [ - "baggage" - ] + "const": "baggage" }, { "description": "Propagate trace context with Jaeger compatible headers", "type": "string", - "enum": [ - "jaeger" - ] + "const": "jaeger" } ] }, @@ -1309,9 +1380,12 @@ }, "endpoint": { "description": "OTLP exporter: OTLP over HTTP compatible endpoint", - "default": "https://localhost:4318", - "type": "string", - "format": "uri" + "type": [ + "string", + "null" + ], + "format": "uri", + "default": "https://localhost:4318" } } }, @@ -1321,30 +1395,22 @@ { "description": "Don't export metrics", "type": "string", - "enum": [ - "none" - ] + "const": "none" }, { "description": "Export metrics to stdout. Only useful for debugging", "type": "string", - "enum": [ - "stdout" - ] + "const": "stdout" }, { "description": "Export metrics to an OpenTelemetry protocol compatible endpoint", "type": "string", - "enum": [ - "otlp" - ] + "const": "otlp" }, { - "description": "Export metrics via Prometheus. An HTTP listener with the `prometheus` resource must be setup to expose the Promethes metrics.", + "description": "Export metrics via Prometheus. An HTTP listener with the `prometheus`\n resource must be setup to expose the Promethes metrics.", "type": "string", - "enum": [ - "prometheus" - ] + "const": "prometheus" } ] }, @@ -1354,38 +1420,50 @@ "properties": { "dsn": { "description": "Sentry DSN", + "type": [ + "string", + "null" + ], + "format": "uri", "examples": [ "https://public@host:port/1" - ], - "type": "string", - "format": "uri" + ] }, "environment": { - "description": "Environment to use when sending events to Sentry\n\nDefaults to `production` if not set.", + "description": "Environment to use when sending events to Sentry\n\n Defaults to `production` if not set.", + "type": [ + "string", + "null" + ], "examples": [ "production" - ], - "type": "string" + ] }, "sample_rate": { - "description": "Sample rate for event submissions\n\nDefaults to `1.0` if not set.", + "description": "Sample rate for event submissions\n\n Defaults to `1.0` if not set.", + "type": [ + "number", + "null" + ], + "format": "float", "examples": [ 0.5 ], - "type": "number", - "format": "float", - "maximum": 1.0, - "minimum": 0.0 + "minimum": 0.0, + "maximum": 1.0 }, "traces_sample_rate": { - "description": "Sample rate for tracing transactions\n\nDefaults to `0.0` if not set.", + "description": "Sample rate for tracing transactions\n\n Defaults to `0.0` if not set.", + "type": [ + "number", + "null" + ], + "format": "float", "examples": [ 0.5 ], - "type": "number", - "format": "float", - "maximum": 1.0, - "minimum": 0.0 + "minimum": 0.0, + "maximum": 1.0 } } }, @@ -1395,36 +1473,42 @@ "properties": { "path": { "description": "Path to the folder which holds the templates", - "type": "string" + "type": [ + "string", + "null" + ] }, "assets_manifest": { "description": "Path to the assets manifest", - "type": "string" + "type": [ + "string", + "null" + ] }, "translations_path": { "description": "Path to the translations", - "type": "string" + "type": [ + "string", + "null" + ] } } }, "EmailConfig": { "description": "Configuration related to sending emails", "type": "object", - "required": [ - "transport" - ], "properties": { "from": { "description": "Email address to use as From when sending emails", - "default": "\"Authentication Service\" ", "type": "string", - "format": "email" + "format": "email", + "default": "\"Authentication Service\" " }, "reply_to": { "description": "Email address to use as Reply-To when sending emails", - "default": "\"Authentication Service\" ", "type": "string", - "format": "email" + "format": "email", + "default": "\"Authentication Service\" " }, "transport": { "description": "What backend should be used when sending emails", @@ -1436,41 +1520,62 @@ }, "mode": { "description": "SMTP transport: Connection mode to the relay", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/EmailSmtpMode" + }, + { + "type": "null" } ] }, "hostname": { "description": "SMTP transport: Hostname to connect to", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/Hostname" + }, + { + "type": "null" } ] }, "port": { - "description": "SMTP transport: Port to connect to. Default is 25 for plain, 465 for TLS and 587 for `StartTLS`", - "type": "integer", + "description": "SMTP transport: Port to connect to. Default is 25 for plain, 465 for TLS\n and 587 for `StartTLS`", + "type": [ + "integer", + "null" + ], "format": "uint16", - "maximum": 65535.0, - "minimum": 1.0 + "minimum": 1, + "maximum": 65535 }, "username": { - "description": "SMTP transport: Username for use to authenticate when connecting to the SMTP server\n\nMust be set if the `password` field is set", - "type": "string" + "description": "SMTP transport: Username for use to authenticate when connecting to the\n SMTP server\n\n Must be set if the `password` field is set", + "type": [ + "string", + "null" + ] }, "password": { - "description": "SMTP transport: Password for use to authenticate when connecting to the SMTP server\n\nMust be set if the `username` field is set", - "type": "string" + "description": "SMTP transport: Password for use to authenticate when connecting to the\n SMTP server\n\n Must be set if the `username` field is set", + "type": [ + "string", + "null" + ] }, "command": { "description": "Sendmail transport: Command to use to send emails", - "default": "sendmail", - "type": "string" + "type": [ + "string", + "null" + ], + "default": "sendmail" } - } + }, + "required": [ + "transport" + ] }, "EmailTransportKind": { "description": "What backend should be used when sending emails", @@ -1478,23 +1583,17 @@ { "description": "Don't send emails anywhere", "type": "string", - "enum": [ - "blackhole" - ] + "const": "blackhole" }, { "description": "Send emails via an SMTP relay", "type": "string", - "enum": [ - "smtp" - ] + "const": "smtp" }, { "description": "Send emails by calling sendmail", "type": "string", - "enum": [ - "sendmail" - ] + "const": "sendmail" } ] }, @@ -1504,23 +1603,17 @@ { "description": "Plain text", "type": "string", - "enum": [ - "plain" - ] + "const": "plain" }, { "description": "`StartTLS` (starts as plain text then upgrade to TLS)", "type": "string", - "enum": [ - "starttls" - ] + "const": "starttls" }, { "description": "TLS", "type": "string", - "enum": [ - "tls" - ] + "const": "tls" } ] }, @@ -1530,26 +1623,38 @@ "properties": { "encryption_file": { "description": "File containing the encryption key for secure cookies.", - "type": "string" + "type": [ + "string", + "null" + ] }, "encryption": { "description": "Encryption key for secure cookies.", + "type": [ + "string", + "null" + ], "examples": [ "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" ], - "type": "string", "pattern": "[0-9a-fA-F]{64}" }, "keys": { "description": "List of private keys to use for signing and encrypting payloads.", - "type": "array", + "type": [ + "array", + "null" + ], "items": { "$ref": "#/definitions/KeyConfig" } }, "keys_dir": { "description": "Directory of private keys to use for signing and encrypting payloads.", - "type": "string" + "type": [ + "string", + "null" + ] } } }, @@ -1558,20 +1663,35 @@ "type": "object", "properties": { "kid": { - "description": "The key ID `kid` of the key as used by JWKs.\n\nIf not given, `kid` will be the key’s RFC 7638 JWK Thumbprint.", - "type": "string" + "description": "The key ID `kid` of the key as used by JWKs.\n\n If not given, `kid` will be the key’s RFC 7638 JWK Thumbprint.", + "type": [ + "string", + "null" + ] }, "password_file": { - "type": "string" + "type": [ + "string", + "null" + ] }, "password": { - "type": "string" + "type": [ + "string", + "null" + ] }, "key_file": { - "type": "string" + "type": [ + "string", + "null" + ] }, "key": { - "type": "string" + "type": [ + "string", + "null" + ] } } }, @@ -1581,44 +1701,42 @@ "properties": { "enabled": { "description": "Whether password-based authentication is enabled", - "default": true, - "type": "boolean" + "type": "boolean", + "default": true }, "schemes": { - "description": "The hashing schemes to use for hashing and validating passwords\n\nThe hashing scheme with the highest version number will be used for hashing new passwords.", + "description": "The hashing schemes to use for hashing and validating passwords\n\n The hashing scheme with the highest version number will be used for\n hashing new passwords.", + "type": "array", + "items": { + "$ref": "#/definitions/HashingScheme" + }, "default": [ { "version": 1, "algorithm": "argon2id" } - ], - "type": "array", - "items": { - "$ref": "#/definitions/HashingScheme" - } + ] }, "minimum_complexity": { - "description": "Score between 0 and 4 determining the minimum allowed password complexity. Scores are based on the ESTIMATED number of guesses needed to guess the password.\n\n- 0: less than 10^2 (100) - 1: less than 10^4 (10'000) - 2: less than 10^6 (1'000'000) - 3: less than 10^8 (100'000'000) - 4: any more than that", - "default": 3, + "description": "Score between 0 and 4 determining the minimum allowed password\n complexity. Scores are based on the ESTIMATED number of guesses\n needed to guess the password.\n\n - 0: less than 10^2 (100)\n - 1: less than 10^4 (10'000)\n - 2: less than 10^6 (1'000'000)\n - 3: less than 10^8 (100'000'000)\n - 4: any more than that", "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 0, + "maximum": 255, + "default": 3 } } }, "HashingScheme": { "description": "Parameters for a password hashing scheme", "type": "object", - "required": [ - "algorithm", - "version" - ], "properties": { "version": { - "description": "The version of the hashing scheme. They must be unique, and the highest version will be used for hashing new passwords.", + "description": "The version of the hashing scheme. They must be unique, and the highest\n version will be used for hashing new passwords.", "type": "integer", "format": "uint16", - "minimum": 0.0 + "minimum": 0, + "maximum": 65535 }, "algorithm": { "description": "The hashing algorithm to use", @@ -1629,25 +1747,38 @@ ] }, "unicode_normalization": { - "description": "Whether to apply Unicode normalization to the password before hashing\n\nDefaults to `false`, and generally recommended to stay false. This is although recommended when importing password hashs from Synapse, as it applies an NFKC normalization to the password before hashing it.", + "description": "Whether to apply Unicode normalization to the password before hashing\n\n Defaults to `false`, and generally recommended to stay false. This is\n although recommended when importing password hashs from Synapse, as it\n applies an NFKC normalization to the password before hashing it.", "type": "boolean" }, "cost": { "description": "Cost for the bcrypt algorithm", - "default": 12, - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint32", - "minimum": 0.0 + "minimum": 0, + "default": 12 }, "secret": { - "description": "An optional secret to use when hashing passwords. This makes it harder to brute-force the passwords in case of a database leak.", - "type": "string" + "description": "An optional secret to use when hashing passwords. This makes it harder\n to brute-force the passwords in case of a database leak.", + "type": [ + "string", + "null" + ] }, "secret_file": { "description": "Same as `secret`, but read from a file.", - "type": "string" + "type": [ + "string", + "null" + ] } - } + }, + "required": [ + "version", + "algorithm" + ] }, "Algorithm": { "description": "A hashing algorithm", @@ -1655,23 +1786,17 @@ { "description": "bcrypt", "type": "string", - "enum": [ - "bcrypt" - ] + "const": "bcrypt" }, { "description": "argon2id", "type": "string", - "enum": [ - "argon2id" - ] + "const": "argon2id" }, { "description": "PBKDF2", "type": "string", - "enum": [ - "pbkdf2" - ] + "const": "pbkdf2" } ] }, @@ -1690,20 +1815,26 @@ }, "homeserver": { "description": "The server name of the homeserver.", - "default": "localhost:8008", - "type": "string" + "type": "string", + "default": "localhost:8008" + }, + "secret_file": { + "type": [ + "string", + "null" + ] + }, + "secret": { + "type": [ + "string", + "null" + ] }, "endpoint": { "description": "The base URL of the homeserver's client API", - "default": "http://localhost:8008/", "type": "string", - "format": "uri" - }, - "secret_file": { - "type": "string" - }, - "secret": { - "type": "string" + "format": "uri", + "default": "http://localhost:8008/" } } }, @@ -1713,30 +1844,22 @@ { "description": "Homeserver is Synapse, version 1.135.0 or newer", "type": "string", - "enum": [ - "synapse" - ] + "const": "synapse" }, { - "description": "Homeserver is Synapse, version 1.135.0 or newer, in read-only mode\n\nThis is meant for testing rolling out Matrix Authentication Service with no risk of writing data to the homeserver.", + "description": "Homeserver is Synapse, version 1.135.0 or newer, in read-only mode\n\n This is meant for testing rolling out Matrix Authentication Service with\n no risk of writing data to the homeserver.", "type": "string", - "enum": [ - "synapse_read_only" - ] + "const": "synapse_read_only" }, { "description": "Homeserver is Synapse, using the legacy API", "type": "string", - "enum": [ - "synapse_legacy" - ] + "const": "synapse_legacy" }, { "description": "Homeserver is Synapse, with the modern API available (>= 1.135.0)", "type": "string", - "enum": [ - "synapse_modern" - ] + "const": "synapse_modern" } ] }, @@ -1814,7 +1937,7 @@ ] }, "registration": { - "description": "Controls how many registrations attempts are permitted based on source address.", + "description": "Controls how many registrations attempts are permitted\n based on source address.", "default": { "burst": 3, "per_second": 0.0008333333333333334 @@ -1857,7 +1980,7 @@ "type": "object", "properties": { "per_ip": { - "description": "Controls how many account recovery attempts are permitted based on source IP address. This can protect against causing e-mail spam to many targets.\n\nNote: this limit also applies to re-sends.", + "description": "Controls how many account recovery attempts are permitted\n based on source IP address.\n This can protect against causing e-mail spam to many targets.\n\n Note: this limit also applies to re-sends.", "default": { "burst": 3, "per_second": 0.0008333333333333334 @@ -1869,7 +1992,7 @@ ] }, "per_address": { - "description": "Controls how many account recovery attempts are permitted based on the e-mail address entered into the recovery form. This can protect against causing e-mail spam to one target.\n\nNote: this limit also applies to re-sends.", + "description": "Controls how many account recovery attempts are permitted\n based on the e-mail address entered into the recovery form.\n This can protect against causing e-mail spam to one target.\n\n Note: this limit also applies to re-sends.", "default": { "burst": 3, "per_second": 0.0002777777777777778 @@ -1884,29 +2007,29 @@ }, "RateLimiterConfiguration": { "type": "object", - "required": [ - "burst", - "per_second" - ], "properties": { "burst": { - "description": "A one-off burst of actions that the user can perform in one go without waiting.", + "description": "A one-off burst of actions that the user can perform\n in one go without waiting.", "type": "integer", "format": "uint32", - "minimum": 1.0 + "minimum": 1 }, "per_second": { - "description": "How quickly the allowance replenishes, in number of actions per second. Can be fractional to replenish slower.", + "description": "How quickly the allowance replenishes, in number of actions per second.\n Can be fractional to replenish slower.", "type": "number", "format": "double" } - } + }, + "required": [ + "burst", + "per_second" + ] }, "LoginRateLimitingConfig": { "type": "object", "properties": { "per_ip": { - "description": "Controls how many login attempts are permitted based on source IP address. This can protect against brute force login attempts.\n\nNote: this limit also applies to password checks when a user attempts to change their own password.", + "description": "Controls how many login attempts are permitted\n based on source IP address.\n This can protect against brute force login attempts.\n\n Note: this limit also applies to password checks when a user attempts to\n change their own password.", "default": { "burst": 3, "per_second": 0.05 @@ -1918,7 +2041,7 @@ ] }, "per_account": { - "description": "Controls how many login attempts are permitted based on the account that is being attempted to be logged into. This can protect against a distributed brute force attack but should be set high enough to prevent someone's account being casually locked out.\n\nNote: this limit also applies to password checks when a user attempts to change their own password.", + "description": "Controls how many login attempts are permitted\n based on the account that is being attempted to be logged into.\n This can protect against a distributed brute force attack\n but should be set high enough to prevent someone's account being\n casually locked out.\n\n Note: this limit also applies to password checks when a user attempts to\n change their own password.", "default": { "burst": 1800, "per_second": 0.5 @@ -1935,7 +2058,7 @@ "type": "object", "properties": { "per_ip": { - "description": "Controls how many email authentication attempts are permitted based on the source IP address. This can protect against causing e-mail spam to many targets.", + "description": "Controls how many email authentication attempts are permitted\n based on the source IP address.\n This can protect against causing e-mail spam to many targets.", "default": { "burst": 5, "per_second": 0.016666666666666666 @@ -1947,7 +2070,7 @@ ] }, "per_address": { - "description": "Controls how many email authentication attempts are permitted based on the e-mail address entered into the authentication form. This can protect against causing e-mail spam to one target.\n\nNote: this limit also applies to re-sends.", + "description": "Controls how many email authentication attempts are permitted\n based on the e-mail address entered into the authentication form.\n This can protect against causing e-mail spam to one target.\n\n Note: this limit also applies to re-sends.", "default": { "burst": 3, "per_second": 0.0002777777777777778 @@ -1959,7 +2082,7 @@ ] }, "emails_per_session": { - "description": "Controls how many authentication emails are permitted to be sent per authentication session. This ensures not too many authentication codes are created for the same authentication session.", + "description": "Controls how many authentication emails are permitted to be sent per\n authentication session. This ensures not too many authentication codes\n are created for the same authentication session.", "default": { "burst": 2, "per_second": 0.0033333333333333335 @@ -1971,7 +2094,7 @@ ] }, "attempt_per_session": { - "description": "Controls how many code authentication attempts are permitted per authentication session. This can protect against brute-forcing the code.", + "description": "Controls how many code authentication attempts are permitted per\n authentication session. This can protect against brute-forcing the\n code.", "default": { "burst": 10, "per_second": 0.016666666666666666 @@ -1987,9 +2110,6 @@ "UpstreamOAuth2Config": { "description": "Upstream OAuth 2.0 providers configuration", "type": "object", - "required": [ - "providers" - ], "properties": { "providers": { "description": "List of OAuth 2.0 providers", @@ -1998,19 +2118,17 @@ "$ref": "#/definitions/Provider" } } - } + }, + "required": [ + "providers" + ] }, "Provider": { "description": "Configuration for one upstream OAuth 2 provider.", "type": "object", - "required": [ - "client_id", - "id", - "token_endpoint_auth_method" - ], "properties": { "enabled": { - "description": "Whether this provider is enabled.\n\nDefaults to `true`", + "description": "Whether this provider is enabled.\n\n Defaults to `true`", "type": "boolean" }, "id": { @@ -2019,28 +2137,50 @@ "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" }, "synapse_idp_id": { - "description": "The ID of the provider that was used by Synapse. In order to perform a Synapse-to-MAS migration, this must be specified.\n\n## For providers that used OAuth 2.0 or OpenID Connect in Synapse\n\n### For `oidc_providers`: This should be specified as `oidc-` followed by the ID that was configured as `idp_id` in one of the `oidc_providers` in the Synapse configuration. For example, if Synapse's configuration contained `idp_id: wombat` for this provider, then specify `oidc-wombat` here.\n\n### For `oidc_config` (legacy): Specify `oidc` here.", - "type": "string" + "description": "The ID of the provider that was used by Synapse.\n In order to perform a Synapse-to-MAS migration, this must be specified.\n\n ## For providers that used OAuth 2.0 or OpenID Connect in Synapse\n\n ### For `oidc_providers`:\n This should be specified as `oidc-` followed by the ID that was\n configured as `idp_id` in one of the `oidc_providers` in the Synapse\n configuration.\n For example, if Synapse's configuration contained `idp_id: wombat` for\n this provider, then specify `oidc-wombat` here.\n\n ### For `oidc_config` (legacy):\n Specify `oidc` here.", + "type": [ + "string", + "null" + ] }, "issuer": { - "description": "The OIDC issuer URL\n\nThis is required if OIDC discovery is enabled (which is the default)", - "type": "string" + "description": "The OIDC issuer URL\n\n This is required if OIDC discovery is enabled (which is the default)", + "type": [ + "string", + "null" + ] }, "human_name": { "description": "A human-readable name for the provider, that will be shown to users", - "type": "string" + "type": [ + "string", + "null" + ] }, "brand_name": { - "description": "A brand identifier used to customise the UI, e.g. `apple`, `google`, `github`, etc.\n\nValues supported by the default template are:\n\n- `apple` - `google` - `facebook` - `github` - `gitlab` - `twitter` - `discord`", - "type": "string" + "description": "A brand identifier used to customise the UI, e.g. `apple`, `google`,\n `github`, etc.\n\n Values supported by the default template are:\n\n - `apple`\n - `google`\n - `facebook`\n - `github`\n - `gitlab`\n - `twitter`\n - `discord`", + "type": [ + "string", + "null" + ] }, "client_id": { "description": "The client ID to use when authenticating with the provider", "type": "string" }, + "client_secret_file": { + "description": "Path to the file containing the client secret. The client secret is used\n by the `client_secret_basic`, `client_secret_post` and\n `client_secret_jwt` authentication methods.", + "type": [ + "string", + "null" + ] + }, "client_secret": { - "description": "The client secret to use when authenticating with the provider\n\nUsed by the `client_secret_basic`, `client_secret_post`, and `client_secret_jwt` methods", - "type": "string" + "description": "Alternative to `client_secret_file`: Reads the client secret directly\n from the config.", + "type": [ + "string", + "null" + ] }, "token_endpoint_auth_method": { "description": "The method to authenticate the client with the provider", @@ -2052,22 +2192,28 @@ }, "sign_in_with_apple": { "description": "Additional parameters for the `sign_in_with_apple` method", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/SignInWithApple" + }, + { + "type": "null" } ] }, "token_endpoint_auth_signing_alg": { - "description": "The JWS algorithm to use when authenticating the client with the provider\n\nUsed by the `client_secret_jwt` and `private_key_jwt` methods", - "allOf": [ + "description": "The JWS algorithm to use when authenticating the client with the\n provider\n\n Used by the `client_secret_jwt` and `private_key_jwt` methods", + "anyOf": [ { "$ref": "#/definitions/JsonWebSignatureAlg" + }, + { + "type": "null" } ] }, "id_token_signed_response_alg": { - "description": "Expected signature for the JWT payload returned by the token authentication endpoint.\n\nDefaults to `RS256`.", + "description": "Expected signature for the JWT payload returned by the token\n authentication endpoint.\n\n Defaults to `RS256`.", "allOf": [ { "$ref": "#/definitions/JsonWebSignatureAlg" @@ -2075,11 +2221,11 @@ ] }, "scope": { - "description": "The scopes to request from the provider\n\nDefaults to `openid`.", + "description": "The scopes to request from the provider\n\n Defaults to `openid`.", "type": "string" }, "discovery_mode": { - "description": "How to discover the provider's configuration\n\nDefaults to `oidc`, which uses OIDC discovery with strict metadata verification", + "description": "How to discover the provider's configuration\n\n Defaults to `oidc`, which uses OIDC discovery with strict metadata\n verification", "allOf": [ { "$ref": "#/definitions/DiscoveryMode" @@ -2087,7 +2233,7 @@ ] }, "pkce_method": { - "description": "Whether to use proof key for code exchange (PKCE) when requesting and exchanging the token.\n\nDefaults to `auto`, which uses PKCE if the provider supports it.", + "description": "Whether to use proof key for code exchange (PKCE) when requesting and\n exchanging the token.\n\n Defaults to `auto`, which uses PKCE if the provider supports it.", "allOf": [ { "$ref": "#/definitions/PkceMethod" @@ -2095,48 +2241,66 @@ ] }, "fetch_userinfo": { - "description": "Whether to fetch the user profile from the userinfo endpoint, or to rely on the data returned in the `id_token` from the `token_endpoint`.\n\nDefaults to `false`.", - "default": false, - "type": "boolean" + "description": "Whether to fetch the user profile from the userinfo endpoint,\n or to rely on the data returned in the `id_token` from the\n `token_endpoint`.\n\n Defaults to `false`.", + "type": "boolean", + "default": false }, "userinfo_signed_response_alg": { - "description": "Expected signature for the JWT payload returned by the userinfo endpoint.\n\nIf not specified, the response is expected to be an unsigned JSON payload.", - "allOf": [ + "description": "Expected signature for the JWT payload returned by the userinfo\n endpoint.\n\n If not specified, the response is expected to be an unsigned JSON\n payload.", + "anyOf": [ { "$ref": "#/definitions/JsonWebSignatureAlg" + }, + { + "type": "null" } ] }, "authorization_endpoint": { - "description": "The URL to use for the provider's authorization endpoint\n\nDefaults to the `authorization_endpoint` provided through discovery", - "type": "string", + "description": "The URL to use for the provider's authorization endpoint\n\n Defaults to the `authorization_endpoint` provided through discovery", + "type": [ + "string", + "null" + ], "format": "uri" }, "userinfo_endpoint": { - "description": "The URL to use for the provider's userinfo endpoint\n\nDefaults to the `userinfo_endpoint` provided through discovery", - "type": "string", + "description": "The URL to use for the provider's userinfo endpoint\n\n Defaults to the `userinfo_endpoint` provided through discovery", + "type": [ + "string", + "null" + ], "format": "uri" }, "token_endpoint": { - "description": "The URL to use for the provider's token endpoint\n\nDefaults to the `token_endpoint` provided through discovery", - "type": "string", + "description": "The URL to use for the provider's token endpoint\n\n Defaults to the `token_endpoint` provided through discovery", + "type": [ + "string", + "null" + ], "format": "uri" }, "jwks_uri": { - "description": "The URL to use for getting the provider's public keys\n\nDefaults to the `jwks_uri` provided through discovery", - "type": "string", + "description": "The URL to use for getting the provider's public keys\n\n Defaults to the `jwks_uri` provided through discovery", + "type": [ + "string", + "null" + ], "format": "uri" }, "response_mode": { "description": "The response mode we ask the provider to use for the callback", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/ResponseMode" + }, + { + "type": "null" } ] }, "claims_imports": { - "description": "How claims should be imported from the `id_token` provided by the provider", + "description": "How claims should be imported from the `id_token` provided by the\n provider", "allOf": [ { "$ref": "#/definitions/ClaimsImports" @@ -2144,26 +2308,31 @@ ] }, "additional_authorization_parameters": { - "description": "Additional parameters to include in the authorization request\n\nOrders of the keys are not preserved.", + "description": "Additional parameters to include in the authorization request\n\n Orders of the keys are not preserved.", "type": "object", "additionalProperties": { "type": "string" } }, "forward_login_hint": { - "description": "Whether the `login_hint` should be forwarded to the provider in the authorization request.\n\nDefaults to `false`.", - "default": false, - "type": "boolean" + "description": "Whether the `login_hint` should be forwarded to the provider in the\n authorization request.\n\n Defaults to `false`.", + "type": "boolean", + "default": false }, "on_backchannel_logout": { - "description": "What to do when receiving an OIDC Backchannel logout request.\n\nDefaults to `do_nothing`.", + "description": "What to do when receiving an OIDC Backchannel logout request.\n\n Defaults to `do_nothing`.", "allOf": [ { "$ref": "#/definitions/OnBackchannelLogout" } ] } - } + }, + "required": [ + "id", + "client_id", + "token_endpoint_auth_method" + ] }, "TokenAuthMethod": { "description": "Authentication methods used against the OAuth 2.0 provider", @@ -2171,61 +2340,51 @@ { "description": "`none`: No authentication", "type": "string", - "enum": [ - "none" - ] + "const": "none" }, { - "description": "`client_secret_basic`: `client_id` and `client_secret` used as basic authorization credentials", + "description": "`client_secret_basic`: `client_id` and `client_secret` used as basic\n authorization credentials", "type": "string", - "enum": [ - "client_secret_basic" - ] + "const": "client_secret_basic" }, { - "description": "`client_secret_post`: `client_id` and `client_secret` sent in the request body", + "description": "`client_secret_post`: `client_id` and `client_secret` sent in the\n request body", "type": "string", - "enum": [ - "client_secret_post" - ] + "const": "client_secret_post" }, { - "description": "`client_secret_jwt`: a `client_assertion` sent in the request body and signed using the `client_secret`", + "description": "`client_secret_jwt`: a `client_assertion` sent in the request body and\n signed using the `client_secret`", "type": "string", - "enum": [ - "client_secret_jwt" - ] + "const": "client_secret_jwt" }, { - "description": "`private_key_jwt`: a `client_assertion` sent in the request body and signed by an asymmetric key", + "description": "`private_key_jwt`: a `client_assertion` sent in the request body and\n signed by an asymmetric key", "type": "string", - "enum": [ - "private_key_jwt" - ] + "const": "private_key_jwt" }, { "description": "`sign_in_with_apple`: a special method for Signin with Apple", "type": "string", - "enum": [ - "sign_in_with_apple" - ] + "const": "sign_in_with_apple" } ] }, "SignInWithApple": { "type": "object", - "required": [ - "key_id", - "team_id" - ], "properties": { "private_key_file": { "description": "The private key file used to sign the `id_token`", - "type": "string" + "type": [ + "string", + "null" + ] }, "private_key": { "description": "The private key used to sign the `id_token`", - "type": "string" + "type": [ + "string", + "null" + ] }, "team_id": { "description": "The Team ID of the Apple Developer Portal", @@ -2235,7 +2394,11 @@ "description": "The key ID of the Apple Developer Portal", "type": "string" } - } + }, + "required": [ + "team_id", + "key_id" + ] }, "DiscoveryMode": { "description": "How to discover the provider's configuration", @@ -2243,49 +2406,37 @@ { "description": "Use OIDC discovery with strict metadata verification", "type": "string", - "enum": [ - "oidc" - ] + "const": "oidc" }, { "description": "Use OIDC discovery with relaxed metadata verification", "type": "string", - "enum": [ - "insecure" - ] + "const": "insecure" }, { "description": "Use a static configuration", "type": "string", - "enum": [ - "disabled" - ] + "const": "disabled" } ] }, "PkceMethod": { - "description": "Whether to use proof key for code exchange (PKCE) when requesting and exchanging the token.", + "description": "Whether to use proof key for code exchange (PKCE) when requesting and\n exchanging the token.", "oneOf": [ { - "description": "Use PKCE if the provider supports it\n\nDefaults to no PKCE if provider discovery is disabled", + "description": "Use PKCE if the provider supports it\n\n Defaults to no PKCE if provider discovery is disabled", "type": "string", - "enum": [ - "auto" - ] + "const": "auto" }, { "description": "Always use PKCE with the S256 challenge method", "type": "string", - "enum": [ - "always" - ] + "const": "always" }, { "description": "Never use PKCE", "type": "string", - "enum": [ - "never" - ] + "const": "never" } ] }, @@ -2293,18 +2444,14 @@ "description": "The response mode we ask the provider to use for the callback", "oneOf": [ { - "description": "`query`: The provider will send the response as a query string in the URL search parameters", + "description": "`query`: The provider will send the response as a query string in the\n URL search parameters", "type": "string", - "enum": [ - "query" - ] + "const": "query" }, { - "description": "`form_post`: The provider will send the response as a POST request with the response parameters in the request body\n\n", + "description": "`form_post`: The provider will send the response as a POST request with\n the response parameters in the request body\n\n ", "type": "string", - "enum": [ - "form_post" - ] + "const": "form_post" } ] }, @@ -2337,7 +2484,7 @@ ] }, "email": { - "description": "Import the email address of the user based on the `email` and `email_verified` claims", + "description": "Import the email address of the user based on the `email` and\n `email_verified` claims", "allOf": [ { "$ref": "#/definitions/EmailImportPreference" @@ -2359,8 +2506,11 @@ "type": "object", "properties": { "template": { - "description": "The Jinja2 template to use for the subject attribute\n\nIf not provided, the default template is `{{ user.sub }}`", - "type": "string" + "description": "The Jinja2 template to use for the subject attribute\n\n If not provided, the default template is `{{ user.sub }}`", + "type": [ + "string", + "null" + ] } } }, @@ -2377,8 +2527,11 @@ ] }, "template": { - "description": "The Jinja2 template to use for the localpart attribute\n\nIf not provided, the default template is `{{ user.preferred_username }}`", - "type": "string" + "description": "The Jinja2 template to use for the localpart attribute\n\n If not provided, the default template is `{{ user.preferred_username }}`", + "type": [ + "string", + "null" + ] }, "on_conflict": { "description": "How to handle conflicts on the claim, default value is `Fail`", @@ -2396,30 +2549,22 @@ { "description": "Ignore the claim", "type": "string", - "enum": [ - "ignore" - ] + "const": "ignore" }, { "description": "Suggest the claim value, but allow the user to change it", "type": "string", - "enum": [ - "suggest" - ] + "const": "suggest" }, { "description": "Force the claim value, but don't fail if it is missing", "type": "string", - "enum": [ - "force" - ] + "const": "force" }, { "description": "Force the claim value, and fail if it is missing", "type": "string", - "enum": [ - "require" - ] + "const": "require" } ] }, @@ -2429,16 +2574,12 @@ { "description": "Fails the sso login on conflict", "type": "string", - "enum": [ - "fail" - ] + "const": "fail" }, { - "description": "Adds the oauth identity link, regardless of whether there is an existing link or not", + "description": "Adds the oauth identity link, regardless of whether there is an existing\n link or not", "type": "string", - "enum": [ - "add" - ] + "const": "add" } ] }, @@ -2455,8 +2596,11 @@ ] }, "template": { - "description": "The Jinja2 template to use for the displayname attribute\n\nIf not provided, the default template is `{{ user.name }}`", - "type": "string" + "description": "The Jinja2 template to use for the displayname attribute\n\n If not provided, the default template is `{{ user.name }}`", + "type": [ + "string", + "null" + ] } } }, @@ -2473,8 +2617,11 @@ ] }, "template": { - "description": "The Jinja2 template to use for the email address attribute\n\nIf not provided, the default template is `{{ user.email }}`", - "type": "string" + "description": "The Jinja2 template to use for the email address attribute\n\n If not provided, the default template is `{{ user.email }}`", + "type": [ + "string", + "null" + ] } } }, @@ -2483,8 +2630,11 @@ "type": "object", "properties": { "template": { - "description": "The Jinja2 template to use for the account name. This name is only used for display purposes.\n\nIf not provided, it will be ignored.", - "type": "string" + "description": "The Jinja2 template to use for the account name. This name is only used\n for display purposes.\n\n If not provided, it will be ignored.", + "type": [ + "string", + "null" + ] } } }, @@ -2494,23 +2644,17 @@ { "description": "Do nothing", "type": "string", - "enum": [ - "do_nothing" - ] + "const": "do_nothing" }, { "description": "Only log out the MAS 'browser session' started by this OIDC session", "type": "string", - "enum": [ - "logout_browser_only" - ] + "const": "logout_browser_only" }, { - "description": "Log out all sessions started by this OIDC session, including MAS 'browser sessions' and client sessions", + "description": "Log out all sessions started by this OIDC session, including MAS\n 'browser sessions' and client sessions", "type": "string", - "enum": [ - "logout_all" - ] + "const": "logout_all" } ] }, @@ -2520,25 +2664,40 @@ "properties": { "service_name": { "description": "A human-readable name. Defaults to the server's address.", - "type": "string" + "type": [ + "string", + "null" + ] }, "policy_uri": { - "description": "Link to a privacy policy, displayed in the footer of web pages and emails. It is also advertised to clients through the `op_policy_uri` OIDC provider metadata.", - "type": "string", + "description": "Link to a privacy policy, displayed in the footer of web pages and\n emails. It is also advertised to clients through the `op_policy_uri`\n OIDC provider metadata.", + "type": [ + "string", + "null" + ], "format": "uri" }, "tos_uri": { - "description": "Link to a terms of service document, displayed in the footer of web pages and emails. It is also advertised to clients through the `op_tos_uri` OIDC provider metadata.", - "type": "string", + "description": "Link to a terms of service document, displayed in the footer of web\n pages and emails. It is also advertised to clients through the\n `op_tos_uri` OIDC provider metadata.", + "type": [ + "string", + "null" + ], "format": "uri" }, "imprint": { - "description": "Legal imprint, displayed in the footer in the footer of web pages and emails.", - "type": "string" + "description": "Legal imprint, displayed in the footer in the footer of web pages and\n emails.", + "type": [ + "string", + "null" + ] }, "logo_uri": { "description": "Logo displayed in some web pages.", - "type": "string", + "type": [ + "string", + "null" + ], "format": "uri" } } @@ -2549,19 +2708,28 @@ "properties": { "service": { "description": "Which service should be used for CAPTCHA protection", - "allOf": [ + "anyOf": [ { "$ref": "#/definitions/CaptchaServiceKind" + }, + { + "type": "null" } ] }, "site_key": { "description": "The site key to use", - "type": "string" + "type": [ + "string", + "null" + ] }, "secret_key": { "description": "The secret key to use", - "type": "string" + "type": [ + "string", + "null" + ] } } }, @@ -2571,23 +2739,17 @@ { "description": "Use Google's reCAPTCHA v2 API", "type": "string", - "enum": [ - "recaptcha_v2" - ] + "const": "recaptcha_v2" }, { "description": "Use Cloudflare Turnstile", "type": "string", - "enum": [ - "cloudflare_turnstile" - ] + "const": "cloudflare_turnstile" }, { "description": "Use ``HCaptcha``", "type": "string", - "enum": [ - "hcaptcha" - ] + "const": "hcaptcha" } ] }, @@ -2596,101 +2758,111 @@ "type": "object", "properties": { "email_change_allowed": { - "description": "Whether users are allowed to change their email addresses. Defaults to `true`.", + "description": "Whether users are allowed to change their email addresses. Defaults to\n `true`.", "type": "boolean" }, "displayname_change_allowed": { - "description": "Whether users are allowed to change their display names. Defaults to `true`.\n\nThis should be in sync with the policy in the homeserver configuration.", + "description": "Whether users are allowed to change their display names. Defaults to\n `true`.\n\n This should be in sync with the policy in the homeserver configuration.", "type": "boolean" }, "password_registration_enabled": { - "description": "Whether to enable self-service password registration. Defaults to `false` if password authentication is enabled.\n\nThis has no effect if password login is disabled.", + "description": "Whether to enable self-service password registration. Defaults to\n `false` if password authentication is enabled.\n\n This has no effect if password login is disabled.", + "type": "boolean" + }, + "password_registration_email_required": { + "description": "Whether self-service password registrations require a valid email.\n Defaults to `true`.\n\n This has no effect if password registration is disabled.", "type": "boolean" }, "password_change_allowed": { - "description": "Whether users are allowed to change their passwords. Defaults to `true`.\n\nThis has no effect if password login is disabled.", + "description": "Whether users are allowed to change their passwords. Defaults to `true`.\n\n This has no effect if password login is disabled.", "type": "boolean" }, "password_recovery_enabled": { - "description": "Whether email-based password recovery is enabled. Defaults to `false`.\n\nThis has no effect if password login is disabled.", + "description": "Whether email-based password recovery is enabled. Defaults to `false`.\n\n This has no effect if password login is disabled.", "type": "boolean" }, "account_deactivation_allowed": { - "description": "Whether users are allowed to delete their own account. Defaults to `true`.", + "description": "Whether users are allowed to delete their own account. Defaults to\n `true`.", "type": "boolean" }, "login_with_email_allowed": { - "description": "Whether users can log in with their email address. Defaults to `false`.\n\nThis has no effect if password login is disabled.", + "description": "Whether users can log in with their email address. Defaults to `false`.\n\n This has no effect if password login is disabled.", "type": "boolean" }, "registration_token_required": { - "description": "Whether registration tokens are required for password registrations. Defaults to `false`.\n\nWhen enabled, users must provide a valid registration token during password registration. This has no effect if password registration is disabled.", + "description": "Whether registration tokens are required for password registrations.\n Defaults to `false`.\n\n When enabled, users must provide a valid registration token during\n password registration. This has no effect if password registration\n is disabled.", "type": "boolean" } } }, "ExperimentalConfig": { - "description": "Configuration sections for experimental options\n\nDo not change these options unless you know what you are doing.", + "description": "Configuration sections for experimental options\n\n Do not change these options unless you know what you are doing.", "type": "object", "properties": { "access_token_ttl": { "description": "Time-to-live of access tokens in seconds. Defaults to 5 minutes.", "type": "integer", "format": "uint64", - "maximum": 86400.0, - "minimum": 60.0 + "minimum": 60, + "maximum": 86400 }, "compat_token_ttl": { - "description": "Time-to-live of compatibility access tokens in seconds. Defaults to 5 minutes.", + "description": "Time-to-live of compatibility access tokens in seconds. Defaults to 5\n minutes.", "type": "integer", "format": "uint64", - "maximum": 86400.0, - "minimum": 60.0 + "minimum": 60, + "maximum": 86400 }, "inactive_session_expiration": { - "description": "Experimetal feature to automatically expire inactive sessions\n\nDisabled by default", - "allOf": [ + "description": "Experimetal feature to automatically expire inactive sessions\n\n Disabled by default", + "anyOf": [ { "$ref": "#/definitions/InactiveSessionExpirationConfig" + }, + { + "type": "null" } ] }, "plan_management_iframe_uri": { - "description": "Experimental feature to show a plan management tab and iframe. This value is passed through \"as is\" to the client without any validation.", - "type": "string" + "description": "Experimental feature to show a plan management tab and iframe.\n This value is passed through \"as is\" to the client without any\n validation.", + "type": [ + "string", + "null" + ] } } }, "InactiveSessionExpirationConfig": { "description": "Configuration options for the inactive session expiration feature", "type": "object", - "required": [ - "ttl" - ], "properties": { "ttl": { "description": "Time after which an inactive session is automatically finished", "type": "integer", "format": "uint64", - "maximum": 7776000.0, - "minimum": 600.0 + "minimum": 600, + "maximum": 7776000 }, "expire_compat_sessions": { "description": "Should compatibility sessions expire after inactivity", - "default": true, - "type": "boolean" + "type": "boolean", + "default": true }, "expire_oauth_sessions": { "description": "Should OAuth 2.0 sessions expire after inactivity", - "default": true, - "type": "boolean" + "type": "boolean", + "default": true }, "expire_user_sessions": { "description": "Should user sessions expire after inactivity", - "default": true, - "type": "boolean" + "type": "boolean", + "default": true } - } + }, + "required": [ + "ttl" + ] } } } \ No newline at end of file diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 1bb04e730..27367b946 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -72,6 +72,7 @@ Make sure your code adheres to our Rust and TypeScript code style by running: - `cargo +nightly fmt` (with the nightly toolchain installed) - `npm run format` in the `frontend` directory + - `make fmt` in the `policies` directory (if changed) When updating SQL queries in the `crates/storage-pg/` crate, you may need to update the `sqlx` introspection data. To do this, make sure to install `cargo-sqlx` (`cargo install sqlx-cli`) and: @@ -86,6 +87,7 @@ While you're developing and before submitting a patch, you'll want to test your - Run `cargo clippy --workspace` to lint the Rust code. - Run `npm run lint` in the `frontend` directory to lint the frontend code. +- Run `make fmt` and `make lint` in the `policies` directory to format and lint the included policy. ### Run the tests @@ -93,6 +95,10 @@ If you haven't already, install [Cargo-Nextest](https://nexte.st/docs/installati - Run the tests to the backend by running `cargo nextest run --workspace`. This requires a connection to a PostgreSQL database, set via the `DATABASE_URL` environment variable. - Run the tests to the frontend by running `npm run test` in the `frontend` directory. +- To run the tests for the included policy, change to the `policies` directory and run one of: + - `make test` (needs Open Policy Agent installed) + - `make PODMAN=1 test` (runs inside a container; needs Podman installed) + - `make DOCKER=1 test` (runs inside a container; needs Docker installed) ## 8. Submit a pull request diff --git a/docs/reference/cli/manage.md b/docs/reference/cli/manage.md index 0f14f1773..d633c4108 100644 --- a/docs/reference/cli/manage.md +++ b/docs/reference/cli/manage.md @@ -23,6 +23,32 @@ $ mas-cli manage add-email $ mas-cli manage verify-email ``` +## `manage promote-admin` + +Make a user admin. + +``` +$ mas-cli manage promote-admin +``` + +**This doesn't make all the users sessions admin, but rather lets the user request admin access in administration tools.** + +## `manage demote-admin` + +Make a user non-admin. + +``` +$ mas-cli manage demote-admin +``` + +## `manage list-admin-users` + +List all users with admin privileges. + +``` +$ mas-cli manage list-admins +``` + ## `manage set-password` Set a user password. @@ -93,8 +119,11 @@ $ mas-cli manage lock-user --deactivate Unlock a user. +Options: +- `--reactivate`: Whether to reactivate the user. + ``` -$ mas-cli manage unlock-user +$ mas-cli manage unlock-user --reactivate ``` ## `manage register-user` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 1f5f4fd11..f614791dd 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -307,6 +307,12 @@ account: # This has no effect if password login is disabled. password_registration_enabled: false + # Whether self-service registrations require a valid email + # + # Defaults to `true` + # This has no effect if password registration is disabled. + password_registration_email_required: true + # Whether users are allowed to change their passwords # # Defaults to `true`. @@ -647,7 +653,8 @@ upstream_oauth2: # The client secret to use to authenticate to the provider # This is only used by the `client_secret_post`, `client_secret_basic` # and `client_secret_jwk` authentication methods - #client_secret: f4f6bb68a0269264877e9cb23b1856ab + client_secret_file: secret + # OR client_secret: f4f6bb68a0269264877e9cb23b1856ab # Which authentication method to use to authenticate to the provider # Supported methods are: @@ -770,7 +777,7 @@ upstream_oauth2: localpart: #action: force #template: "{{ user.preferred_username }}" - + # 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. diff --git a/docs/topics/access-token.md b/docs/topics/access-token.md index 1d898cd89..16acc4a10 100644 --- a/docs/topics/access-token.md +++ b/docs/topics/access-token.md @@ -8,7 +8,7 @@ This can be run from anywhere, not necessarily from the host where MAS is runnin sh ./misc/device-code-grant.sh [synapse-url] ... ``` -This will prompt you to open a URL in your browser, finish the authentication flow, and print the access token. +This will prompt you to open a URL in your browser, finish the authentication flow, and print the access and refresh tokens. This can be used to get access to the MAS admin API: diff --git a/docs/topics/admin-api.md b/docs/topics/admin-api.md index 75d5e2b0a..8aa992c76 100644 --- a/docs/topics/admin-api.md +++ b/docs/topics/admin-api.md @@ -46,7 +46,9 @@ If admin API is enabled, MAS will also serve the specification at `/api/spec.jso ## Authentication -All requests to the admin API are gated using access tokens obtained using OAuth 2.0 grants. +All requests to the admin API are gated either using access tokens obtained using OAuth 2.0 grants, +or using personal access tokens (which must currently be issued through the Admin API). + They must have the [`urn:mas:admin`](../reference/scopes.md#urnmasadmin) scope. ### User-interactive tools diff --git a/docs/topics/authorization.md b/docs/topics/authorization.md index d2d7ab29c..1bdeb8207 100644 --- a/docs/topics/authorization.md +++ b/docs/topics/authorization.md @@ -129,6 +129,33 @@ It may also be used in the future as a foundation for a new Application Service This works by presenting the client credentials to get back an access token. The simplest type of client credentials is a client ID and client secret pair, but MAS also supports client authentication with a JWT ([RFC 7523]), which is a robust way to authenticate clients without a shared secret. +## Personal sessions (personal access tokens) + +Personal access tokens are a credential that can be issued to give access to a user, +with predefined scopes and a predefined expiry time. +Either before or after expiry, the owner of the token can regenerate it, which produces a new +access token with the same scopes but a new expiry time. + +Personal access tokens are intended to fulfill two basic use cases: + +1. an easy way to obtain a clean token for your own user, for use in automation and scripts; +2. a way to obtain a token for administrative access of another user, either for ad-hoc administrative operations or to set up a bot or similar service. + +In the future, users will be able to create their own personal access tokens, but this is currently not implemented +so (1) is currently not supported. + +For now, personal access tokens must be created, regenerated and revoked by administrators through the [Admin API], satisfying use case (2). +[Element Admin](https://github.com/element-hq/element-admin), available by default in Element Server Suite, can be used to do this interactively. +You can also use the online beta deployment at [admin-beta.element.dev](https://admin-beta.element.dev/). + +### Validity + +Personal sessions can be used so long as: + +- the owner (creator) of the token is still an active and unlocked user (or static OAuth 2 client); and +- the actor (target user, or user being controlled by the token) has not been deactivated. Though the actor is allowed to be locked. + + [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108 [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749 [RFC 7523]: https://datatracker.ietf.org/doc/html/rfc7523 @@ -140,3 +167,4 @@ The simplest type of client credentials is a client ID and client secret pair, b [`urn:synapse:admin:*`]: ../reference/scopes.md#urnsynapseadmin [`urn:mas:graphql:*`]: ../reference/scopes.md#urnmasgraphql [`urn:mas:admin`]: ../reference/scopes.md#urnmasadmin +[Admin API]: ./admin-api.md diff --git a/docs/topics/policy.md b/docs/topics/policy.md index 9155c2211..7ea1c2c93 100644 --- a/docs/topics/policy.md +++ b/docs/topics/policy.md @@ -24,11 +24,10 @@ As such, they usually can be bypassed through the admin API or the CLI if needed ### User attributes -The policy is evaluated in three different scenarios: +The policy is evaluated in the following different scenarios: - - [`register.rego`]: During user registration, either with password credentials or with an upstream OAuth 2.0 provider. This calls the [`email.rego`] and [`password.rego`] policies as well. + - [`register.rego`]: During user registration, either with password credentials or with an upstream OAuth 2.0 provider. This calls the [`email.rego`] policy as well. - [`email.rego`]: When a user adds a new email address to their account. - - [`password.rego`]: When a user changes their password. ### Client registration @@ -69,8 +68,7 @@ This is especially important as in the future it will make it possible to implem To understand the authorization process and how sessions are created, refer to the [authorization and sessions](./authorization.md) section. -[`register.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/register.rego -[`email.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/email.rego -[`password.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/password.rego -[`client_registration.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/client_registration.rego -[`authorization_grant.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/authorization_grant.rego +[`register.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/register/register.rego +[`email.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/email/email.rego +[`client_registration.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/client_registration/client_registration.rego +[`authorization_grant.rego`]: https://github.com/element-hq/matrix-authentication-service/blob/main/policies/authorization_grant/authorization_grant.rego diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 000000000..b6f27f135 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/.storybook/locales.ts b/frontend/.storybook/locales.ts index 090812bf0..a9f88823c 100644 --- a/frontend/.storybook/locales.ts +++ b/frontend/.storybook/locales.ts @@ -27,7 +27,7 @@ export type LocalazyMetadata = { }; const localazyMetadata: LocalazyMetadata = { - projectUrl: "https://localazy.com/p/matrix-authentication-service", + projectUrl: "https://localazy.com/p/matrix-authentication-service!v1.6", baseLocale: "en", languages: [ { @@ -172,21 +172,21 @@ const localazyMetadata: LocalazyMetadata = { file: "frontend.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", - "hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", - "nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" + "cs": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", + "da": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", + "de": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", + "en": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", + "et": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", + "fi": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", + "fr": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", + "hu": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", + "nb_NO": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", + "nl": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", + "pt": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", + "ru": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", + "sv": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", + "uk": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", + "zh#Hans": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" } }, { @@ -194,21 +194,21 @@ const localazyMetadata: LocalazyMetadata = { file: "file.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", - "hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", - "nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" + "cs": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", + "da": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", + "de": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", + "en": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", + "et": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", + "fi": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", + "fr": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", + "hu": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", + "nb_NO": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", + "nl": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", + "pt": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", + "ru": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", + "sv": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", + "uk": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", + "zh#Hans": "https://delivery.localazy.com/_a67480892591190493723a576eac/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" } } ] diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index d895a0bc9..b4ffa1976 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -9,7 +9,7 @@ import type { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { stories: ["../{src,stories}/**/*.stories.@(js|jsx|ts|tsx)"], - addons: ["storybook-react-i18next", "@storybook/addon-docs"], + addons: ["@storybook/addon-docs"], framework: "@storybook/react-vite", diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index a9abadc7d..7ba9e4218 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -4,15 +4,11 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. -import type { - ArgTypes, - Decorator, - Parameters, - Preview, -} from "@storybook/react-vite"; +import type { Decorator, Preview } from "@storybook/react-vite"; import { TooltipProvider } from "@vector-im/compound-web"; import { initialize, mswLoader } from "msw-storybook-addon"; -import { useLayoutEffect } from "react"; +import { useEffect, useLayoutEffect } from "react"; +import { I18nextProvider } from "react-i18next"; import "../src/shared.css"; import i18n, { setupI18n } from "../src/i18n"; import { DummyRouter } from "../src/test-utils/router"; @@ -31,37 +27,12 @@ initialize( setupI18n(); -export const parameters: Parameters = { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -}; - -export const globalTypes = { - theme: { - name: "Theme", - defaultValue: "system", - description: "Global theme for components", - toolbar: { - icon: "circlehollow", - title: "Theme", - items: [ - { title: "System", value: "system", icon: "browser" }, - { title: "Light", value: "light", icon: "sun" }, - { title: "Light (high contrast)", value: "light-hc", icon: "sun" }, - { title: "Dark", value: "dark", icon: "moon" }, - { title: "Dark (high contrast)", value: "dark-hc", icon: "moon" }, - ], - }, - }, -} satisfies ArgTypes; - -const allThemesClasses = globalTypes.theme.toolbar.items.map( - ({ value }) => `cpd-theme-${value}`, -); +const allThemesClasses = [ + "cpd-theme-light", + "cpd-theme-light-hc", + "cpd-theme-dark", + "cpd-theme-dark-hc", +]; const ThemeSwitcher: React.FC<{ theme: string; @@ -86,6 +57,27 @@ const withThemeProvider: Decorator = (Story, context) => { ); }; +const LocaleSwitcher: React.FC<{ + locale: string; +}> = ({ locale }) => { + useEffect(() => { + i18n.changeLanguage(locale); + }, [locale]); + + return null; +}; + +const withI18nProvider: Decorator = (Story, context) => { + return ( + <> + + + + + + ); +}; + const withDummyRouter: Decorator = (Story, _context) => { return ( @@ -102,28 +94,58 @@ const withTooltipProvider: Decorator = (Story, _context) => { ); }; -export const decorators: Decorator[] = [ - withThemeProvider, - withDummyRouter, - withTooltipProvider, -]; - -const locales = Object.fromEntries( - localazyMetadata.languages.map(({ language, name, localizedName }) => [ - language, - `${localizedName} (${name})`, - ]), -); - const preview: Preview = { + loaders: [mswLoader], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, + decorators: [ + withI18nProvider, + withThemeProvider, + withDummyRouter, + withTooltipProvider, + ], + globalTypes: { + theme: { + name: "Theme", + description: "Global theme for components", + toolbar: { + icon: "circlehollow", + title: "Theme", + items: [ + { title: "System", value: "system", icon: "browser" }, + { title: "Light", value: "light", icon: "sun" }, + { title: "Light (high contrast)", value: "light-hc", icon: "sun" }, + { title: "Dark", value: "dark", icon: "moon" }, + { title: "Dark (high contrast)", value: "dark-hc", icon: "moon" }, + ], + }, + }, + + locale: { + name: "Locale", + description: "Locale for the app", + toolbar: { + title: "Language", + icon: "globe", + items: localazyMetadata.languages.map( + ({ language, localizedName, name }) => ({ + title: `${localizedName} (${name})`, + value: language, + }), + ), + }, + }, + }, initialGlobals: { locale: localazyMetadata.baseLocale, - locales, + theme: "system", }, - parameters: { - i18n, - }, - loaders: [mswLoader], tags: ["autodocs"], }; diff --git a/frontend/.storybook/public/mockServiceWorker.js b/frontend/.storybook/public/mockServiceWorker.js index 7e23102e0..2f658e919 100644 --- a/frontend/.storybook/public/mockServiceWorker.js +++ b/frontend/.storybook/public/mockServiceWorker.js @@ -7,8 +7,8 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.11.1' -const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' +const PACKAGE_VERSION = '2.11.6' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() @@ -71,11 +71,6 @@ addEventListener('message', async function (event) { break } - case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId) - break - } - case 'CLIENT_CLOSED': { activeClientIds.delete(clientId) @@ -94,6 +89,8 @@ addEventListener('message', async function (event) { }) addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + // Bypass navigation requests. if (event.request.mode === 'navigate') { return @@ -110,23 +107,29 @@ addEventListener('fetch', function (event) { // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). + // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { return } const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId)) + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) }) /** * @param {FetchEvent} event * @param {string} requestId + * @param {number} requestInterceptedAt */ -async function handleRequest(event, requestId) { +async function handleRequest(event, requestId, requestInterceptedAt) { const client = await resolveMainClient(event) const requestCloneForEvents = event.request.clone() - const response = await getResponse(event, client, requestId) + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise @@ -202,9 +205,10 @@ async function resolveMainClient(event) { * @param {FetchEvent} event * @param {Client | undefined} client * @param {string} requestId + * @param {number} requestInterceptedAt * @returns {Promise} */ -async function getResponse(event, client, requestId) { +async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). const requestClone = event.request.clone() @@ -255,6 +259,7 @@ async function getResponse(event, client, requestId) { type: 'REQUEST', payload: { id: requestId, + interceptedAt: requestInterceptedAt, ...serializedRequest, }, }, diff --git a/frontend/i18next-parser.config.ts b/frontend/i18next-parser.config.ts deleted file mode 100644 index 1fa452605..000000000 --- a/frontend/i18next-parser.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024, 2025 New Vector Ltd. -// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -// Please see LICENSE files in the repository root for full details. - -import type { UserConfig } from "i18next-parser"; - -export default { - keySeparator: ".", - pluralSeparator: ":", - defaultNamespace: "frontend", - lexers: { - ts: [ - { - lexer: "JavascriptLexer", - functions: ["t"], - namespaceFunctions: ["useTranslation", "withTranslation"], - }, - ], - tsx: [ - { - lexer: "JsxLexer", - functions: ["t"], - namespaceFunctions: ["useTranslation", "withTranslation"], - }, - ], - }, - locales: ["en"], - output: "locales/$LOCALE.json", - input: ["src/**/*.{ts,tsx}"], - sort: true, -} satisfies UserConfig; diff --git a/frontend/i18next.config.ts b/frontend/i18next.config.ts new file mode 100644 index 000000000..04453eece --- /dev/null +++ b/frontend/i18next.config.ts @@ -0,0 +1,18 @@ +// 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. + +import { defineConfig } from "i18next-cli"; + +export default defineConfig({ + locales: ["en"], + extract: { + input: "src/**/*.{ts,tsx}", + output: "locales/{{language}}.json", + defaultNS: false, + pluralSeparator: ":", + keySeparator: ".", + sort: true, + }, +}); diff --git a/frontend/knip.config.ts b/frontend/knip.config.ts index b5e382961..66b84227d 100644 --- a/frontend/knip.config.ts +++ b/frontend/knip.config.ts @@ -7,7 +7,12 @@ import type { KnipConfig } from "knip"; export default { entry: ["src/main.tsx", "src/swagger.ts", "src/routes/*"], - ignore: ["src/gql/*", "src/routeTree.gen.ts", ".storybook/locales.ts"], + ignore: [ + "src/gql/*", + "src/routeTree.gen.ts", + ".storybook/locales.ts", + "i18next.config.ts", + ], ignoreDependencies: [ // This is used by the tailwind PostCSS plugin, but not detected by knip "postcss-nesting", diff --git a/frontend/locales/de.json b/frontend/locales/de.json index b5bcfe437..75c8493ed 100644 --- a/frontend/locales/de.json +++ b/frontend/locales/de.json @@ -5,7 +5,7 @@ "clear": "Löschen", "close": "Schließen", "collapse": "Zusammenbruch", - "confirm": " Bestätigen Sie", + "confirm": "Bestätigen", "continue": "Weiter", "edit": "Bearbeiten", "expand": "Erweitern", @@ -40,16 +40,16 @@ "account_password": "Kontokennwort", "contact_info": "Kontaktinformation", "delete_account": { - "alert_description": "Dieses Konto wird dauerhaft gelöscht und Sie haben keinen Zugriff mehr auf Ihre Nachrichten.", - "alert_title": "Sie sind dabei, alle Ihre Daten zu verlieren", + "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ätigen Sie, dass Sie Ihr Konto löschen möchten:\n\nSie können Ihr Konto nicht reaktivieren\nSie können sich nicht mehr anmelden\nNiemand kann Ihren Benutzernamen (MXID) wieder verwenden, auch Sie nicht.\nSie verlassen alle Räume und Direktnachrichten, in denen Sie sich befinden\nSie werden vom Identitätsserver entfernt und niemand kann Sie mit Ihrer E-Mail-Adresse oder Telefonnummer finden\n\nIhre alten Nachrichten sind für Empfänger weiterhin sichtbar. Möchten Sie Ihre gesendeten Nachrichten vor zukünftigen Chatroom-Besuchern verbergen?", + "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_title": "Dieses Konto löschen?", "erase_checkbox_label": "Ja, alle meine Nachrichten vor neuen Mitgliedern verbergen", - "incorrect_password": "Falsches Passwort, bitte versuchen Sie es erneut", - "mxid_label": "Bestätigen Sie Ihre Matrix-ID ({{ mxid }})", - "mxid_mismatch": "Dieser Wert stimmt nicht mit Ihrer Matrix-ID überein", - "password_label": "Geben Sie Ihr Passwort ein, um fortzufahren" + "incorrect_password": "Falsches Passwort, versuch's nochmal", + "mxid_label": "Bestätige deine Matrix-ID ({{ mxid }})", + "mxid_mismatch": "Dieser Wert passt nicht zu deiner Matrix-ID.", + "password_label": "Gib dein Passwort ein, um weiterzumachen" }, "edit_profile": { "display_name_help": "Dies ist der öffentliche Nutzername.", @@ -79,7 +79,7 @@ "title": "Diese E-Mailadresse existiert bereits" }, "email_exists_error": "Die eingegebene E-Mail-Adresse ist diesem Konto bereits zugeordnet", - "email_field_help": "Fügen Sie eine alternative E-Mail-Adresse hinzu, mit der Sie auf dieses Konto zugreifen können.", + "email_field_help": "Gib eine alternative E-Mail-Adresse an, mit der du auf dieses Konto zugreifen kannst.", "email_field_label": "E-Mail-Adresse hinzufügen", "email_in_use_error": "Die eingegebene E-Mail wird bereits verwendet", "email_invalid_alert": { @@ -87,8 +87,8 @@ "title": "Ungültige Email-Adresse" }, "email_invalid_error": "Die eingegebene E-Mail-Adresse ist ungültig", - "incorrect_password_error": "Falsches Passwort, bitte versuchen Sie es erneut", - "password_confirmation": "Bestätigen Sie Ihr Kontopasswort, um diese E-Mail-Adresse hinzuzufügen" + "incorrect_password_error": "Falsches Passwort, versuch's nochmal", + "password_confirmation": "Bestätige dein Passwort, um diese E-Mail-Adresse hinzuzufügen." }, "app_sessions_list": { "error": "App-Sitzungen konnten nicht geladen werden", @@ -103,8 +103,8 @@ "body:other": "{{count}} aktive Sitzungen", "heading": "Browser", "no_active_sessions": { - "default": "Sie sind bei keinem Webbrowser angemeldet.", - "inactive_90_days": "Alle Ihre Sitzungen waren in den letzten 90 Tagen aktiv." + "default": "Du bist in keinem Webbrowser angemeldet.", + "inactive_90_days": "Alle deine Sitzungen waren in den letzten 90 Tagen aktiv." }, "view_all_button": "Alle anzeigen" }, @@ -125,19 +125,19 @@ "heading": "Die E-Mail-Adresse {{email}} wird bereits verwendet." }, "end_session_button": { - "confirmation_modal_title": "Sind Sie sicher, dass Sie diese Sitzung abmelden möchten?", + "confirmation_modal_title": "Möchtest du diese Sitzung wirklich beenden?", "text": "Gerät entfernen" }, "error": { "hideDetails": "Details ausblenden", "showDetails": "Details anzeigen", - "subtitle": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut", + "subtitle": "Ein unerwarteter Fehler ist aufgetreten, bitte versuch's nochmal.", "title": "Etwas ist schief gelaufen" }, "error_boundary_title": "Etwas ist schief gelaufen", "errors": { "field_required": "Dieses Feld ist ein Pflichtfeld", - "rate_limit_exceeded": "Sie haben in kurzer Zeit zu viele Anfragen gestellt. Bitte warten Sie einige Minuten und versuchen Sie es erneut." + "rate_limit_exceeded": "Du hast in kurzer Zeit zu viele Anfragen gestellt. Warte bitte ein paar Minuten und versuch's nochmal." }, "last_active": { "active_date": "Aktiv {{relativeDate}}", @@ -146,13 +146,13 @@ }, "nav": { "devices": "Geräte", - "plan": "Plan", + "plan": "Abo", "profile": "Profil", "sessions": "Sitzungen", "settings": "Einstellungen" }, "not_found_alert_title": "Nicht gefunden.", - "not_logged_in_alert": "Sie sind nicht angemeldet.", + "not_logged_in_alert": "Du bist nicht angemeldet.", "oauth2_client_detail": { "details_title": "Geräte Information", "id": "Client-ID", @@ -172,15 +172,15 @@ "current_password_label": "Aktuelles Passwort", "failure": { "description": { - "account_locked": "Ihr Konto ist gesperrt und kann derzeit nicht wiederhergestellt werden. Wenn dies unerwartet auftritt, wenden Sie sich bitte an Ihren Serveradministrator.", - "expired_recovery_ticket": "Der Wiederherstellungslink ist abgelaufen. Bitte starten Sie den Kontowiederherstellungsprozess erneut von Anfang an.", - "invalid_new_password": "Das neue Passwort, das Sie gewählt haben, ist ungültig; es entspricht möglicherweise nicht der konfigurierten Sicherheitsrichtlinie.", - "no_current_password": "Sie haben kein aktuelles Passwort.", - "no_such_recovery_ticket": "Der Wiederherstellungslink ist ungültig. Wenn Sie den Link aus der Wiederherstellungs-E-Mail kopiert haben, überprüfen Sie bitte, ob der vollständige Link kopiert wurde.", + "account_locked": "Dein Konto ist gesperrt und kann im Moment nicht wiederhergestellt werden. Wenn du das nicht erwartet hast, wende dich bitte an deinen Server-Admin.", + "expired_recovery_ticket": "Der Link zur Kontowiederherstellung ist abgelaufen. Bitte fang den Prozess noch mal von vorne an.", + "invalid_new_password": "Das neue Passwort, das du gewählt hast, ist ungültig; es entspricht möglicherweise nicht den Sicherheitsrichtlinien.", + "no_current_password": "Du hast kein aktuelles Passwort.", + "no_such_recovery_ticket": "Der Link zum Wiederherstellen ist nicht gültig. Wenn du den Link aus der E-Mail zum Wiederherstellen kopiert hast, schau bitte nach, ob du den vollständigen Link kopiert hast.", "password_changes_disabled": "Passwortänderungen sind deaktiviert.", "recovery_ticket_already_used": "Der Wiederherstellungslink wurde bereits verwendet. Er kann nicht erneut verwendet werden.", - "unspecified": "Dies könnte ein vorübergehendes Problem sein. Bitte versuchen Sie es später erneut. Wenn das Problem weiterhin besteht, wenden Sie sich bitte an Ihren Serveradministrator.", - "wrong_password": "Das Passwort, das Sie als Ihr aktuelles Passwort eingegeben haben, ist falsch. Bitte versuchen Sie es erneut." + "unspecified": "Das könnte ein vorübergehendes Problem sein, also versuch's später nochmal. Wenn das Problem weiterhin besteht, wende dich bitte an deinen Server-Admin.", + "wrong_password": "Das Passwort, das du als dein aktuelles Passwort angegeben hast, ist falsch. Versuch's bitte nochmal." }, "title": "Aktualisierung des Passworts fehlgeschlagen" }, @@ -188,25 +188,25 @@ "new_password_label": "Neues Passwort", "passwords_match": "Passwörter stimmen überein!", "passwords_no_match": "Passwörter stimmen nicht überein", - "subtitle": "Wählen Sie ein neues Passwort für Ihren Account.", + "subtitle": "Such dir ein neues Passwort für dein Konto aus.", "success": { - "description": "Ihr Passwort wurde erfolgreich aktualisiert.", - "title": "Passwort aktualisiert" + "description": "Dein Passwort wurde geändert.", + "title": "Passwort geändert" }, - "title": "Ändern Sie Ihr Passwort" + "title": "Ändere dein Passwort" }, "password_reset": { "consumed": { - "subtitle": "Um ein neues Passwort zu erstellen, beginnen Sie von vorne und wählen Sie „Passwort vergessen“.", - "title": "Der Link zum Zurücksetzen Ihres Passworts wurde bereits verwendet" + "subtitle": "Um ein neues Passwort zu erstellen, fang einfach von vorne an und wähle „Passwort vergessen“.", + "title": "Der Link zum Zurücksetzen deines Passworts wurde bereits verwendet" }, "expired": { "resend_email": "E-Mail erneut senden", - "subtitle": "Fordern Sie eine neue E-Mail an, die an folgende Adresse gesendet wird: {{email}}", - "title": "Der Link zum Zurücksetzen Ihres Passworts ist abgelaufen" + "subtitle": "Eine neue E-Mail anfordern, die an folgende Adresse gesendet wird: {{email}}", + "title": "Der Link zum Zurücksetzen deines Passworts ist abgelaufen" }, - "subtitle": "Wählen Sie ein neues Passwort für Ihren Account.", - "title": "Ihr Passwort zurücksetzen" + "subtitle": "Such dir ein neues Passwort für dein Konto aus.", + "title": "Setze dein Passwort zurück" }, "password_strength": { "placeholder": "Passwortstärke", @@ -218,20 +218,20 @@ "4": "Sehr starkes Passwort" }, "suggestion": { - "all_uppercase": "Schreiben Sie einige, aber nicht alle Buchstaben groß.", - "another_word": "Fügen Sie weitere Wörter hinzu, die weniger gebräuchlich sind.", - "associated_years": "Vermeiden Sie Jahre, die mit Ihnen in Verbindung gebracht werden.", - "capitalization": "Schreiben Sie mehr als den ersten Buchstaben groß.", - "dates": "Vermeiden Sie Daten und Jahre, die mit Ihnen in Verbindung gebracht werden.", - "l33t": "Vermeiden Sie vorhersehbare Buchstabenersetzungen wie '@' für 'a'.", - "longer_keyboard_pattern": "Verwenden Sie längere Eingaben und ändern Sie die Tipprichtung mehrmals.", - "no_need": "Sie können sichere Passwörter erstellen, ohne Symbole, Zahlen oder Großbuchstaben zu verwenden.", - "pwned": "Wenn Sie dieses Passwort woanders verwenden, sollten Sie es ändern.", - "recent_years": "Vermeiden Sie die letzten Jahre.", - "repeated": "Vermeiden Sie Wort- und Zeichenwiederholungen.", - "reverse_words": "Vermeiden Sie umgekehrte Schreibweisen gängiger Wörter.", - "sequences": "Vermeiden Sie gängige Zeichenfolgen.", - "use_words": "Verwenden Sie mehrere Wörter, vermeiden Sie jedoch gebräuchliche Ausdrücke." + "all_uppercase": "Schreib ein paar Buchstaben groß, aber nicht alle.", + "another_word": "Füge mehr Wörter hinzu, die weniger gebräuchlich sind.", + "associated_years": "Vermeide Jahre, die mit dir in Verbindung stehen.", + "capitalization": "Schreib mehr als nur den ersten Buchstaben groß.", + "dates": "Vermeide Daten und Jahreszahlen, die mit dir in Verbindung stehen.", + "l33t": "Vermeide vorhersehbare Buchstabenersetzungen wie „@“ für „a“.", + "longer_keyboard_pattern": "Benutz längere Tastaturmuster und wechsel mehrmals die Schreibrichtung.", + "no_need": "Du kannst starke Passwörter erstellen, ohne Symbole, Zahlen oder Großbuchstaben zu benutzen.", + "pwned": "Wenn du dieses Passwort auch woanders benutzt, solltest du es ändern.", + "recent_years": "Vermeide die letzten Jahre.", + "repeated": "Vermeide es, Wörter und Zeichen zu wiederholen.", + "reverse_words": "Vermeide es, gängige Wörter rückwärts zu schreiben.", + "sequences": "Vermeide gängige Zeichenfolgen.", + "use_words": "Benutze mehrere Wörter, aber vermeide gängige Redewendungen." }, "too_weak": "Dieses Passwort ist zu schwach", "warning": { @@ -241,12 +241,12 @@ "extended_repeat": "Wiederholte Zeichenmuster wie „abcabcabc“ sind leicht zu erraten.", "key_pattern": "Kurze Eingaben sind leicht zu erraten.", "names_by_themselves": "Einzelne Vor- oder Nachnamen sind leicht zu erraten.", - "pwned": "Ihr Passwort wurde durch eine Datenpanne im Internet preisgegeben.", + "pwned": "Dein Passwort wurde durch eine Datenpanne im Internet preisgegeben.", "recent_years": "Die letzten Jahre sind leicht zu erraten.", "sequences": "Gängige Zeichenfolgen wie „abc“ sind leicht zu erraten.", "similar_to_common": "Dies ähnelt einem häufig verwendeten Passwort.", "simple_repeat": "Sich wiederholende Zeichen wie \"aaa\" sind leicht zu erraten.", - "straight_row": "Gerade Tastenreihen auf Ihrer Tastatur sind leicht zu erraten.", + "straight_row": "Gerade Reihen von Tasten auf deiner Tastatur sind leicht zu erraten.", "top_hundred": "Dies ist ein häufig verwendetes Passwort.", "top_ten": "Dies ist ein häufig verwendetes Passwort.", "user_inputs": "Es sollten keine persönlichen oder seitenbezogenen Daten vorhanden sein.", @@ -256,32 +256,32 @@ "reset_cross_signing": { "button": "Identität zurücksetzen", "cancelled": { - "description_1": "Sie können dieses Fenster schließen und zur App zurückkehren, um fortzufahren.", - "description_2": "Wenn Sie überall abgemeldet sind und Ihren Wiederherstellungscode vergessen haben, müssen Sie Ihre Identität trotzdem zurücksetzen.", + "description_1": "Du kannst dieses Fenster schließen und zur App zurückgehen, um weiterzumachen.", + "description_2": "Wenn du dich überall abgemeldet hast und deinen Wiederherstellungs-Schlüssel nicht mehr weißt, musst du deine Identität zurücksetzen.", "heading": "Identitätszurücksetzung abgebrochen." }, - "description": "Wenn Sie nirgendwo anders angemeldet sind und alle Wiederherstellungsschlüssel verloren haben, müssen Sie Ihre Krypto-Identität zurücksetzen, bevor Sie die App weiterverwenden können", + "description": "Wenn du auf keinem anderen Gerät angemeldet bist und deinen Wiederherstellungs-Schlüssel verloren hast, musst du deine Identität zurücksetzen, um die App weiter nutzen zu können.", "effect_list": { - "negative_1": "Sie werden Ihren bestehenden Nachrichtenverlauf verlieren", - "negative_2": "Sie müssen alle Ihre vorhandenen Geräte und Kontakte erneut überprüfen.", - "neutral_1": "Sie verlieren jeglichen Nachrichtenverlauf, der nur auf dem Server gespeichert ist", - "neutral_2": "Sie müssen alle Ihre vorhandenen Geräte und Kontakte erneut überprüfen.", - "positive_1": "Ihre Kontodaten, Kontakte, Einstellungen und Chat-Liste werden gespeichert" + "negative_1": "Du verlierst deine bestehenden Chats.", + "negative_2": "Du musst alle deine Geräte und Kontakte nochmal verifizieren.", + "neutral_1": "Du verlierst alle Nachrichten, die nur auf dem Server gespeichert sind.", + "neutral_2": "Du musst alle deine Geräte und Kontakte nochmal verifizieren.", + "positive_1": "Deine Kontodaten, Kontakte, Einstellungen und Chat-Liste bleiben erhalten." }, "failure": { - "description": "Dies könnte ein vorübergehendes Problem sein. Bitte versuchen Sie es später erneut. Wenn das Problem weiterhin besteht, wenden Sie sich bitte an Ihren Serveradministrator.", + "description": "Das könnte ein vorübergehendes Problem sein, also versuch's später nochmal. Wenn das Problem weiterhin besteht, wende dich bitte an deinen Server-Admin.", "heading": "Zurücksetzen der Krypto-Identität konnte nicht zugelassen werden", "title": "Krypto-Identität konnte nicht zugelassen werden" }, "finish_reset": "Reset beenden", - "heading": "Setzen Sie Ihre Identität zurück für den Fall, dass Sie sie nicht anders bestätigen können", + "heading": "Erstelle eine neue Identität, solltest du sie nicht auf andere Weise bestätigen können.", "start_reset": "Reset starten", "success": { - "description": "Das Zurücksetzen der Identität wurde für die nächsten {{minutes}} Minuten genehmigt. Sie können dieses Fenster schließen und zur App zurückkehren, um fortzufahren.", - "heading": "Die Identität wurde erfolgreich zurückgesetzt. Kehren Sie zur App zurück, um den Vorgang abzuschließen.", + "description": "Das Zurücksetzen der Identität wurde für die nächsten {{minutes}} Minuten genehmigt. Du kannst dieses Fenster schließen und zur App zurückkehren, um fortzufahren.", + "heading": "Identität erfolgreich zurückgesetzt. Geh zurück zur App, um den Vorgang abzuschließen.", "title": "Das Zurücksetzen der Krypto-Identität ist vorübergehend erlaubt" }, - "warning": "Setzen Sie Ihre Identität nur zurück, wenn Sie keinen Zugriff auf ein anderes angemeldetes Gerät haben und Ihren Wiederherstellungsschlüssel verloren haben." + "warning": "Setze deine Identität nur zurück, wenn du keinen Zugriff auf ein anderes angemeldetes Gerät hast und deinen Wiederherstellungsschlüssel verloren hast." }, "selectable_session": { "label": "Sitzung auswählen" @@ -301,7 +301,7 @@ "name_for_platform": "{{name}} für {{platform}}", "scopes_label": "Berechtigungsumfang", "set_device_name": { - "help": "Geben Sie einen Namen ein, der Ihnen hilft, dieses Gerät zu identifizieren.", + "help": "Gib einen Namen ein, mit dem du dieses Gerät leicht wiederfindest.", "label": "Gerätename", "title": "Gerätename bearbeiten" }, @@ -324,17 +324,17 @@ "unknown_route": "Unbekannte Route {{route}}", "unverified_email_alert": { "button": "Überprüfen und verifizieren", - "text:one": "Sie haben {{count}} nicht verifizierte E-Mail-Adresse.", - "text:other": "Sie haben {{count}} nicht verifizierte E-Mail-Adressen.", + "text:one": "Du hast {{count}} unverifizierte E-Mail-Adresse.", + "text:other": "Du hast {{count}} unverifizierte E-Mail-Adressen.", "title": "Nicht verifizierte E-Mail-Adresse" }, "user_email": { - "cant_delete_primary": "Wählen Sie eine andere primäre E-Mail-Adresse, um diese zu löschen.", + "cant_delete_primary": "Wähle eine andere primäre E-Mail-Adresse aus, um diese zu löschen.", "delete_button_confirmation_modal": { "action": "E-Mail löschen", "body": "Diese E-Mail löschen?", - "incorrect_password": "Falsches Passwort, bitte versuchen Sie es erneut", - "password_confirmation": "Bestätigen Sie Ihr Kontopasswort, um diese E-Mail-Adresse zu löschen" + "incorrect_password": "Falsches Passwort, versuch's nochmal", + "password_confirmation": "Bestätige dein Passwort, um diese E-Mail-Adresse zu löschen." }, "delete_button_title": "E-Mail-Adresse entfernen", "email": "E-Mail", @@ -357,29 +357,29 @@ "user_sessions_overview": { "active_sessions:one": "{{count}} aktive Sitzung", "active_sessions:other": "{{count}} aktive Sitzungen", - "heading": "Wo Sie angemeldet sind", + "heading": "Wo du angemeldet bist", "no_active_sessions": { - "default": "Sie sind bei keiner Anwendung angemeldet.", - "inactive_90_days": "Alle Ihre Sitzungen waren in den letzten 90 Tagen aktiv." + "default": "Du bist bei keiner Anwendung angemeldet.", + "inactive_90_days": "Alle deine Sitzungen waren in den letzten 90 Tagen aktiv." } }, "verify_email": { "code_expired_alert": { - "description": "Der Code ist abgelaufen. Bitte fordern Sie einen neuen Code an.", + "description": "Der Code ist abgelaufen. Bitte fordere einen neuen Code an.", "title": "Code abgelaufen" }, "code_field_error": "Code nicht erkannt", "code_field_label": "6-stelliger Code", "code_field_wrong_shape": "Der Code muss 6-stellig sein", "email_sent_alert": { - "description": "Geben Sie unten den neuen Code ein.", + "description": "Gib den neuen Code unten ein.", "title": "Neuer Code gesendet" }, - "enter_code_prompt": "Geben Sie den 6-stelligen Code ein, der an {{email}} gesendet wurde", - "heading": "Bestätigen Sie Ihre E-Mail-Adresse", + "enter_code_prompt": "Gib den 6-stelligen Code ein, der an {{email}} gesendet wurde", + "heading": "Bestätige deine E-Mail", "invalid_code_alert": { - "description": "Überprüfen Sie den Code, der an Ihre E-Mail-Adresse gesendet wurde, und aktualisieren Sie die folgenden Felder, um fortzufahren.", - "title": "Sie haben einen falschen Code eingegeben" + "description": "Überprüfe den Code, der an deine E-Mail-Adresse gesendet wurde, und aktualisiere die folgenden Felder, um fortzufahren.", + "title": "Du hast den falschen Code eingegeben." }, "resend_code": "Code erneut senden", "resend_email": "E-Mail erneut senden", @@ -389,13 +389,13 @@ }, "mas": { "scope": { - "edit_profile": "Ihr Profil und Ihre Kontaktdaten bearbeiten", - "manage_sessions": "Ihre Geräte und Sitzungen verwalten", + "edit_profile": "Bearbeite dein Profil und deine Kontaktdaten", + "manage_sessions": "Verwalte deine Geräte und Sitzungen", "mas_admin": "Beliebige Benutzer verwalten", - "send_messages": "Nachrichten in Ihrem Namen senden", + "send_messages": "Neue Nachrichten in deinem Namen senden", "synapse_admin": "Den Synapse-Homeserver verwalten", - "view_messages": "Ihre vorhandenen Nachrichten und Daten anzeigen", - "view_profile": "Ihre Profilinformationen und Kontaktdaten anzeigen" + "view_messages": "Zeig deine vorhandenen Nachrichten und Daten an", + "view_profile": "Deine Profilinfos und Kontaktdaten anzeigen" } } } \ No newline at end of file diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 929b1a99c..3fe5250fa 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -287,9 +287,6 @@ "delete_button_title": "Remove email address", "email": "Email" }, - "user_email_list": { - "no_primary_email_alert": "No primary email address" - }, "user_sessions_overview": { "heading": "Where you're signed in", "no_active_sessions": { diff --git a/frontend/locales/ru.json b/frontend/locales/ru.json index 85ddf2008..743597672 100644 --- a/frontend/locales/ru.json +++ b/frontend/locales/ru.json @@ -147,7 +147,7 @@ }, "nav": { "devices": "Устройства", - "plan": "Plan", + "plan": "Тарифный план", "profile": "Профиль", "sessions": "Сессии", "settings": "Настройки" @@ -302,9 +302,9 @@ "name_for_platform": "{{name}} для {{platform}}", "scopes_label": "Области", "set_device_name": { - "help": "Set a name that will help you identify this device.", - "label": "Device name", - "title": "Edit device name" + "help": "Установите имя, которое поможет вам идентифицировать это устройство.", + "label": "Имя устройства", + "title": "Переименовать устройство" }, "signed_in_date": "Вошёл ", "signed_in_label": "Вошёл в систему", diff --git a/frontend/locales/zh-Hans.json b/frontend/locales/zh-Hans.json index 57e1b4cb3..ec5b8534f 100644 --- a/frontend/locales/zh-Hans.json +++ b/frontend/locales/zh-Hans.json @@ -273,7 +273,7 @@ "title": "无法允许加密身份" }, "finish_reset": "完成重置", - "heading": "重置加密身份", + "heading": "如果你无法通过其它方式确认请重置身份", "start_reset": "开始重置", "success": { "description": "身份重置已获批准,有效时间为 {{minutes}} 分钟。您可以关闭此窗口并返回应用继续操作。", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cae710f08..3475e7b1c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,131 +8,74 @@ "name": "mas-frontend", "version": "0.0.0", "dependencies": { - "@fontsource/inconsolata": "^5.2.6", - "@fontsource/inter": "^5.2.6", - "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.14", - "@tanstack/react-query": "^5.85.5", - "@tanstack/react-router": "^1.131.27", - "@vector-im/compound-design-tokens": "5.0.2", - "@vector-im/compound-web": "^8.2.0", + "@fontsource/inconsolata": "^5.2.8", + "@fontsource/inter": "^5.2.8", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@tanstack/react-query": "^5.90.7", + "@tanstack/react-router": "^1.131.44", + "@vector-im/compound-design-tokens": "6.0.0", + "@vector-im/compound-web": "^8.2.4", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "classnames": "^2.5.1", "date-fns": "^4.1.0", - "i18next": "^25.4.2", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-i18next": "^15.7.2", - "swagger-ui-dist": "^5.27.1", + "i18next": "^25.6.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.3.0", + "swagger-ui-dist": "^5.29.5", "valibot": "^1.1.0", "vaul": "^1.1.2" }, "devDependencies": { - "@biomejs/biome": "^2.1.2", + "@biomejs/biome": "^2.3.2", "@browser-logos/chrome": "^2.0.0", "@browser-logos/firefox": "^3.0.10", "@browser-logos/safari": "^2.1.0", - "@codecov/vite-plugin": "^1.9.1", - "@graphql-codegen/cli": "^5.0.7", - "@graphql-codegen/client-preset": "^4.8.3", + "@graphql-codegen/cli": "^6.0.1", + "@graphql-codegen/client-preset": "^5.1.1", "@graphql-codegen/typescript-msw": "^3.0.1", - "@storybook/addon-docs": "^9.1.3", - "@storybook/react-vite": "^9.1.3", - "@tanstack/react-query-devtools": "^5.85.5", - "@tanstack/react-router-devtools": "^1.131.27", - "@tanstack/router-plugin": "^1.131.27", - "@testing-library/jest-dom": "^6.8.0", + "@storybook/addon-docs": "^10.0.8", + "@storybook/react-vite": "^10.0.8", + "@tanstack/react-query-devtools": "^5.90.2", + "@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.3.0", - "@types/react": "19.1.10", - "@types/react-dom": "19.1.7", + "@types/node": "^24.9.1", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.2", "@types/swagger-ui-dist": "^3.30.6", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.21", "browserslist-to-esbuild": "^2.1.1", "graphql": "^16.11.0", - "happy-dom": "^18.0.1", - "i18next-parser": "^9.3.0", - "knip": "^5.62.0", - "msw": "^2.11.1", + "happy-dom": "^20.0.4", + "i18next-cli": "^1.22.1", + "knip": "^5.66.4", + "msw": "^2.11.6", "msw-storybook-addon": "^2.0.5", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "postcss-nesting": "^13.0.2", "rimraf": "^6.0.1", - "storybook": "^9.0.1", - "storybook-react-i18next": "4.0.11", - "tailwindcss": "^3.4.17", - "typescript": "^5.9.2", - "vite": "6.3.5", + "storybook": "^10.0.5", + "tailwindcss": "^3.4.18", + "typescript": "^5.9.3", + "vite": "7.2.2", "vite-plugin-compression": "^0.5.1", - "vite-plugin-graphql-codegen": "^3.6.1", + "vite-plugin-graphql-codegen": "^3.7.0", "vite-plugin-manifest-sri": "^0.2.0", - "vitest": "^3.2.3" + "vitest": "^3.2.4" } }, - "node_modules/@actions/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", - "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/exec": "^1.1.1", - "@actions/http-client": "^2.0.1" - } - }, - "node_modules/@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/io": "^1.0.1" - } - }, - "node_modules/@actions/github": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", - "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/http-client": "^2.2.0", - "@octokit/core": "^5.0.1", - "@octokit/plugin-paginate-rest": "^9.2.2", - "@octokit/plugin-rest-endpoint-methods": "^10.4.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "undici": "^5.28.5" - } - }, - "node_modules/@actions/http-client": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", - "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "^0.0.6", - "undici": "^5.25.4" - } - }, - "node_modules/@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@adobe/css-tools": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", - "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, "license": "MIT" }, @@ -204,9 +147,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, "license": "MIT", "engines": { @@ -214,21 +157,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -243,24 +187,15 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -299,29 +234,19 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "engines": { @@ -331,21 +256,12 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -379,15 +295,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -462,9 +378,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -482,26 +398,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -672,9 +589,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz", - "integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", "dev": true, "license": "MIT", "dependencies": { @@ -688,18 +605,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.27.1", - "globals": "^11.1.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -726,13 +643,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", - "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -860,9 +778,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, "license": "MIT", "dependencies": { @@ -892,9 +810,9 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz", - "integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", "dev": true, "license": "MIT", "dependencies": { @@ -1049,9 +967,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1073,17 +991,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -1091,13 +1010,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1114,9 +1034,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz", - "integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz", + "integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -1130,20 +1050,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.1.2", - "@biomejs/cli-darwin-x64": "2.1.2", - "@biomejs/cli-linux-arm64": "2.1.2", - "@biomejs/cli-linux-arm64-musl": "2.1.2", - "@biomejs/cli-linux-x64": "2.1.2", - "@biomejs/cli-linux-x64-musl": "2.1.2", - "@biomejs/cli-win32-arm64": "2.1.2", - "@biomejs/cli-win32-x64": "2.1.2" + "@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" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.2.tgz", - "integrity": "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz", + "integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==", "cpu": [ "arm64" ], @@ -1158,9 +1078,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.2.tgz", - "integrity": "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A==", + "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==", "cpu": [ "x64" ], @@ -1175,9 +1095,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.2.tgz", - "integrity": "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw==", + "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==", "cpu": [ "arm64" ], @@ -1192,9 +1112,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.2.tgz", - "integrity": "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ==", + "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==", "cpu": [ "arm64" ], @@ -1209,9 +1129,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.2.tgz", - "integrity": "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA==", + "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==", "cpu": [ "x64" ], @@ -1226,9 +1146,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.2.tgz", - "integrity": "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA==", + "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==", "cpu": [ "x64" ], @@ -1243,9 +1163,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.2.tgz", - "integrity": "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA==", + "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==", "cpu": [ "arm64" ], @@ -1260,9 +1180,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.2.tgz", - "integrity": "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz", + "integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==", "cpu": [ "x64" ], @@ -1294,61 +1214,6 @@ "integrity": "sha512-diidPiK62E4hlAh0dyLfWQDZXi2SSAGiOuw6iqD1x8ztw7L/Sz3He46FhcxEzYa1hKi1blCkjnKDjqw6rQfgcA==", "dev": true }, - "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", - "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cookie": "^0.7.2" - } - }, - "node_modules/@bundled-es-modules/statuses": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", - "dev": true, - "license": "ISC", - "dependencies": { - "statuses": "^2.0.1" - } - }, - "node_modules/@codecov/bundler-plugin-core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.9.1.tgz", - "integrity": "sha512-dt3ic7gMswz4p/qdkYPVJwXlLiLsz55rBBn2I7mr0HTG8pCoLRqnANJIwo5WrqGBZgPyVSMPBqBra6VxLWfDyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/core": "^1.10.1", - "@actions/github": "^6.0.0", - "chalk": "4.1.2", - "semver": "^7.5.4", - "unplugin": "^1.10.1", - "zod": "^3.22.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@codecov/vite-plugin": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@codecov/vite-plugin/-/vite-plugin-1.9.1.tgz", - "integrity": "sha512-S6Yne7comVulJ1jD3T7rCfYFHPR0zUjAYoLjUDPXNJCUrdzWJdf/ak/UepE7TicqQG+yBa6eb5WusqcPgg+1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@codecov/bundler-plugin-core": "^1.9.1", - "unplugin": "^1.10.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "vite": "4.x || 5.x || 6.x" - } - }, "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", @@ -1396,21 +1261,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", + "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.2", + "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", + "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", "dev": true, "license": "MIT", "optional": true, @@ -1419,9 +1284,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "optional": true, @@ -1430,9 +1295,9 @@ } }, "node_modules/@envelop/core": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.2.3.tgz", - "integrity": "sha512-KfoGlYD/XXQSc3BkM1/k15+JQbkQ4ateHazeZoWl9P71FsLTDXSjGy6j7QqfhpIDSbxNISqhPMfZHYSbDFOofQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.3.1.tgz", + "integrity": "sha512-n29V3vRqXvPcG76C8zE482LQykk0P66zv1mjpk7aHeGe9qnh8AzB/RvoX5SVFwApJQPp0ixob8NoYXg4FHKMGA==", "dev": true, "license": "MIT", "dependencies": { @@ -1474,9 +1339,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -1491,9 +1356,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -1508,9 +1373,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -1525,9 +1390,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -1542,9 +1407,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -1559,9 +1424,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -1576,9 +1441,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -1593,9 +1458,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -1610,9 +1475,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -1627,9 +1492,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -1644,9 +1509,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -1661,9 +1526,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -1678,9 +1543,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -1695,9 +1560,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -1712,9 +1577,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -1729,9 +1594,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -1746,9 +1611,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -1763,9 +1628,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -1780,9 +1645,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -1797,9 +1662,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -1814,9 +1679,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -1830,10 +1695,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -1848,9 +1730,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -1865,9 +1747,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -1882,9 +1764,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -1899,39 +1781,39 @@ } }, "node_modules/@fastify/busboy": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", - "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", "dev": true, "license": "MIT" }, "node_modules/@floating-ui/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", - "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", - "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.1", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react": { - "version": "0.27.12", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.12.tgz", - "integrity": "sha512-kKlWNrpIQxF1B/a2MZvE0/uyKby4960yjO91W7nVyNKmmfNi62xU9HCjL1M1eWzx/LFj/VPSwJVbwQk9Pq/68A==", + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.1.3", - "@floating-ui/utils": "^0.2.9", + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { @@ -1940,12 +1822,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", - "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -1953,39 +1835,42 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@fontsource/inconsolata": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.6.tgz", - "integrity": "sha512-TRGh7bN+BN/oP8qD1IYe8REXM/0Uw3jbuERSncA/ZF6mqKFEOeTt6PR2T3xK7G+65N9pn2p0ablamdboee2nFQ==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.8.tgz", + "integrity": "sha512-lIZW+WOZYpUH91g9r6rYYhfTmptF3YPPM54ZOs8IYVeeL4SeiAu4tfj7mdr8llYEq31DLYgi6JtGIJa192gB0Q==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, "node_modules/@fontsource/inter": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.6.tgz", - "integrity": "sha512-CZs9S1CrjD0jPwsNy9W6j0BhsmRSQrgwlTNkgQXTsAeDRM42LBRLo3eo9gCzfH4GvV7zpyf78Ozfl773826csw==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, "node_modules/@graphql-codegen/add": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-5.0.3.tgz", - "integrity": "sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-6.0.0.tgz", + "integrity": "sha512-biFdaURX0KTwEJPQ1wkT6BRgNasqgQ5KbCI1a3zwtLtO7XTo7/vKITPylmiU27K5DSOWYnY/1jfSqUAEBuhZrQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/plugin-helpers": "^6.0.0", "tslib": "~2.6.0" }, + "engines": { + "node": ">=16" + }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } @@ -1998,18 +1883,18 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/cli": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-5.0.7.tgz", - "integrity": "sha512-h/sxYvSaWtxZxo8GtaA8SvcHTyViaaPd7dweF/hmRDpaQU1o3iU3EZxlcJ+oLTunU0tSMFsnrIXm/mhXxI11Cw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-6.0.1.tgz", + "integrity": "sha512-6iP91joxb7phdicDrIF8Cv9ah2QpPVXUUu7rbOaQKvqey+QKYmHcxGCi9r5/7p4lUiHZPQvfB7xDHURHQca1SA==", "dev": true, "license": "MIT", "dependencies": { "@babel/generator": "^7.18.13", "@babel/template": "^7.18.10", "@babel/types": "^7.18.13", - "@graphql-codegen/client-preset": "^4.8.2", - "@graphql-codegen/core": "^4.0.2", - "@graphql-codegen/plugin-helpers": "^5.1.1", + "@graphql-codegen/client-preset": "^5.0.0", + "@graphql-codegen/core": "^5.0.0", + "@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-tools/apollo-engine-loader": "^8.0.0", "@graphql-tools/code-file-loader": "^8.0.0", "@graphql-tools/git-loader": "^8.0.0", @@ -2017,20 +1902,19 @@ "@graphql-tools/graphql-file-loader": "^8.0.0", "@graphql-tools/json-file-loader": "^8.0.0", "@graphql-tools/load": "^8.1.0", - "@graphql-tools/prisma-loader": "^8.0.0", "@graphql-tools/url-loader": "^8.0.0", "@graphql-tools/utils": "^10.0.0", + "@inquirer/prompts": "^7.8.2", "@whatwg-node/fetch": "^0.10.0", "chalk": "^4.1.0", - "cosmiconfig": "^8.1.3", - "debounce": "^1.2.0", + "cosmiconfig": "^9.0.0", + "debounce": "^2.0.0", "detect-indent": "^6.0.0", "graphql-config": "^5.1.1", - "inquirer": "^8.0.0", "is-glob": "^4.0.1", - "jiti": "^1.17.1", + "jiti": "^2.3.0", "json-to-pretty-yaml": "^1.2.2", - "listr2": "^4.0.5", + "listr2": "^9.0.0", "log-symbols": "^4.0.0", "micromatch": "^4.0.5", "shell-quote": "^1.7.3", @@ -2059,21 +1943,59 @@ } } }, - "node_modules/@graphql-codegen/client-preset": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.8.3.tgz", - "integrity": "sha512-QpEsPSO9fnRxA6Z66AmBuGcwHjZ6dYSxYo5ycMlYgSPzAbyG8gn/kWljofjJfWqSY+T/lRn+r8IXTH14ml24vQ==", + "node_modules/@graphql-codegen/cli/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/cli/node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@graphql-codegen/client-preset": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-5.1.1.tgz", + "integrity": "sha512-d7a4KdZJBOPt/O55JneBz9WwvpWar/P5yyxfjZvvoRErXPRsWtswLp+CBKKPkRcEIz9MXfTdQ1GL3kQg16DLfg==", + "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", - "@graphql-codegen/add": "^5.0.3", - "@graphql-codegen/gql-tag-operations": "4.0.17", - "@graphql-codegen/plugin-helpers": "^5.1.1", - "@graphql-codegen/typed-document-node": "^5.1.2", - "@graphql-codegen/typescript": "^4.1.6", - "@graphql-codegen/typescript-operations": "^4.6.1", - "@graphql-codegen/visitor-plugin-common": "^5.8.0", + "@graphql-codegen/add": "^6.0.0", + "@graphql-codegen/gql-tag-operations": "5.0.3", + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/typed-document-node": "^6.0.2", + "@graphql-codegen/typescript": "^5.0.2", + "@graphql-codegen/typescript-operations": "^5.0.2", + "@graphql-codegen/visitor-plugin-common": "^6.1.0", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^10.0.0", "@graphql-typed-document-node/core": "3.2.0", @@ -2100,17 +2022,20 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-4.0.2.tgz", - "integrity": "sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-5.0.0.tgz", + "integrity": "sha512-vLTEW0m8LbE4xgRwbFwCdYxVkJ1dBlVJbQyLb9Q7bHnVFgHAP982Xo8Uv7FuPBmON+2IbTjkCqhFLHVZbqpvjQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-tools/schema": "^10.0.0", "@graphql-tools/utils": "^10.0.0", "tslib": "~2.6.0" }, + "engines": { + "node": ">=16" + }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } @@ -2123,14 +2048,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.17.tgz", - "integrity": "sha512-2pnvPdIG6W9OuxkrEZ6hvZd142+O3B13lvhrZ48yyEBh2ujtmKokw0eTwDHtlXUqjVS0I3q7+HB2y12G/m69CA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-5.0.3.tgz", + "integrity": "sha512-G6YqeDMMuwMvAtlW+MUaQDoYgQtBuBrfp89IOSnj7YXqSc/TMOma3X5XeXM4/oeNDQyfm2A66j5H8DYf04mJZg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/visitor-plugin-common": "5.8.0", + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/visitor-plugin-common": "6.1.0", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" @@ -2150,9 +2075,9 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/plugin-helpers": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.1.1.tgz", - "integrity": "sha512-28GHODK2HY1NhdyRcPP3sCz0Kqxyfiz7boIZ8qIxFYmpLYnlDgiYok5fhFLVSZihyOpCs4Fa37gVHf/Q4I2FEg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.0.0.tgz", + "integrity": "sha512-Z7P89vViJvQakRyMbq/JF2iPLruRFOwOB6IXsuSvV/BptuuEd7fsGPuEf8bdjjDxUY0pJZnFN8oC7jIQ8p9GKA==", "dev": true, "license": "MIT", "dependencies": { @@ -2178,16 +2103,19 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/schema-ast": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-4.1.0.tgz", - "integrity": "sha512-kZVn0z+th9SvqxfKYgztA6PM7mhnSZaj4fiuBWvMTqA+QqQ9BBed6Pz41KuD/jr0gJtnlr2A4++/0VlpVbCTmQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-5.0.0.tgz", + "integrity": "sha512-jn7Q3PKQc0FxXjbpo9trxzlz/GSFQWxL042l0iC8iSbM/Ar+M7uyBwMtXPsev/3Razk+osQyreghIz0d2+6F7Q==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-tools/utils": "^10.0.0", "tslib": "~2.6.0" }, + "engines": { + "node": ">=16" + }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } @@ -2200,13 +2128,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typed-document-node": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.1.2.tgz", - "integrity": "sha512-jaxfViDqFRbNQmfKwUY8hDyjnLTw2Z7DhGutxoOiiAI0gE/LfPe0LYaVFKVmVOOD7M3bWxoWfu4slrkbWbUbEw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-6.0.2.tgz", + "integrity": "sha512-nqcD23F87jLPQ1P2jJaepNAa4SY8Xy2soacPyQMwvxWtbRSXlg/LBUjtbEkCaU2SuLoa4L3w8VPuGoQ3EWUzeg==", "dev": true, + "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/visitor-plugin-common": "5.8.0", + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/visitor-plugin-common": "6.1.0", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", "tslib": "~2.6.0" @@ -2222,18 +2151,19 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/@graphql-codegen/typescript": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.1.6.tgz", - "integrity": "sha512-vpw3sfwf9A7S+kIUjyFxuvrywGxd4lmwmyYnnDVjVE4kSQ6Td3DpqaPTy8aNQ6O96vFoi/bxbZS2BW49PwSUUA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-5.0.2.tgz", + "integrity": "sha512-OJYXpS9SRf4VFzqu3ZH/RmTftGhAVTCmscH63iPlvTlCT8NBmpSHdZ875AEa38LugdL8XgUcGsI3pprP3e5j/w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/schema-ast": "^4.0.2", - "@graphql-codegen/visitor-plugin-common": "5.8.0", + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/schema-ast": "^5.0.0", + "@graphql-codegen/visitor-plugin-common": "6.1.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -2507,6 +2437,16 @@ "node": ">=8" } }, + "node_modules/@graphql-codegen/typescript-msw/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@graphql-codegen/typescript-msw/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -2552,15 +2492,15 @@ } }, "node_modules/@graphql-codegen/typescript-operations": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.6.1.tgz", - "integrity": "sha512-k92laxhih7s0WZ8j5WMIbgKwhe64C0As6x+PdcvgZFMudDJ7rPJ/hFqJ9DCRxNjXoHmSjnr6VUuQZq4lT1RzCA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-5.0.2.tgz", + "integrity": "sha512-i2nSJ5a65H+JgXwWvEuYehVYUImIvrHk3PTs+Fcj+OjZFvDl2qBziIhr6shCjV0KH9IZ6Y+1v4TzkxZr/+XFjA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/typescript": "^4.1.6", - "@graphql-codegen/visitor-plugin-common": "5.8.0", + "@graphql-codegen/plugin-helpers": "^6.0.0", + "@graphql-codegen/typescript": "^5.0.2", + "@graphql-codegen/visitor-plugin-common": "6.1.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -2592,19 +2532,19 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.8.0.tgz", - "integrity": "sha512-lC1E1Kmuzi3WZUlYlqB4fP6+CvbKH9J+haU1iWmgsBx5/sO2ROeXJG4Dmt8gP03bI2BwjiwV5WxCEMlyeuzLnA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.1.0.tgz", + "integrity": "sha512-AvGO1pe+b/kAa7+WBDlNDXOruRZWv/NnhLHgTggiW2XWRv33biuzg4cF1UTdpR2jmESZzJU4kXngLLX8RYJWLA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.1.0", + "@graphql-codegen/plugin-helpers": "^6.0.0", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.0.0", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", + "dependency-graph": "^1.0.0", "graphql-tag": "^2.11.0", "parse-filepath": "^1.0.2", "tslib": "~2.6.0" @@ -2616,6 +2556,16 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/@graphql-codegen/visitor-plugin-common/node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -2634,13 +2584,13 @@ } }, "node_modules/@graphql-tools/apollo-engine-loader": { - "version": "8.0.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.20.tgz", - "integrity": "sha512-m5k9nXSyjq31yNsEqDXLyykEjjn3K3Mo73oOKI+Xjy8cpnsgbT4myeUJIYYQdLrp7fr9Y9p7ZgwT5YcnwmnAbA==", + "version": "8.0.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.22.tgz", + "integrity": "sha512-ssD2wNxeOTRcUEkuGcp0KfZAGstL9YLTe/y3erTDZtOs2wL1TJESw8NVAp+3oUHPeHKBZQB4Z6RFEbPgMdT2wA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/fetch": "^0.10.0", "sync-fetch": "0.6.0-2", "tslib": "^2.4.0" @@ -2653,13 +2603,13 @@ } }, "node_modules/@graphql-tools/batch-execute": { - "version": "9.0.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.17.tgz", - "integrity": "sha512-i7BqBkUP2+ex8zrQrCQTEt6nYHQmIey9qg7CMRRa1hXCY2X8ZCVjxsvbsi7gOLwyI/R3NHxSRDxmzZevE2cPLg==", + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.19.tgz", + "integrity": "sha512-VGamgY4PLzSx48IHPoblRw0oTaBa7S26RpZXt0Y4NN90ytoE0LutlpB2484RbkfcTjv9wa64QD474+YP1kEgGA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", "tslib": "^2.8.1" @@ -2672,14 +2622,14 @@ } }, "node_modules/@graphql-tools/code-file-loader": { - "version": "8.1.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.20.tgz", - "integrity": "sha512-GzIbjjWJIc04KWnEr8VKuPe0FA2vDTlkaeub5p4lLimljnJ6C0QSkOyCUnFmsB9jetQcHm0Wfmn/akMnFUG+wA==", + "version": "8.1.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.22.tgz", + "integrity": "sha512-FSka29kqFkfFmw36CwoQ+4iyhchxfEzPbXOi37lCEjWLHudGaPkXc3RyB9LdmBxx3g3GHEu43a5n5W8gfcrMdA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.19", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/graphql-tag-pluck": "8.3.21", + "@graphql-tools/utils": "^10.9.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -2692,16 +2642,16 @@ } }, "node_modules/@graphql-tools/delegate": { - "version": "10.2.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.19.tgz", - "integrity": "sha512-aaCGAALTQmKctHwumbtz0c5XehGjYLSfoDx1IB2vdPt76Q0MKz2AiEDlENgzTVr4JHH7fd9YNrd+IO3D8tFlIg==", + "version": "10.2.23", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.23.tgz", + "integrity": "sha512-xrPtl7f1LxS+B6o+W7ueuQh67CwRkfl+UKJncaslnqYdkxKmNBB4wnzVcW8ZsRdwbsla/v43PtwAvSlzxCzq2w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/batch-execute": "^9.0.17", - "@graphql-tools/executor": "^1.4.7", - "@graphql-tools/schema": "^10.0.11", - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/batch-execute": "^9.0.19", + "@graphql-tools/executor": "^1.4.9", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", "@repeaterjs/repeater": "^3.0.6", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", @@ -2733,13 +2683,13 @@ } }, "node_modules/@graphql-tools/executor": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.7.tgz", - "integrity": "sha512-U0nK9jzJRP9/9Izf1+0Gggd6K6RNRsheFo1gC/VWzfnsr0qjcOSS9qTjY0OTC5iTPt4tQ+W5Zpw/uc7mebI6aA==", + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.9.tgz", + "integrity": "sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", @@ -2771,19 +2721,36 @@ } }, "node_modules/@graphql-tools/executor-graphql-ws": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.5.tgz", - "integrity": "sha512-gI/D9VUzI1Jt1G28GYpvm5ckupgJ5O8mi5Y657UyuUozX34ErfVdZ81g6oVcKFQZ60LhCzk7jJeykK48gaLhDw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.7.tgz", + "integrity": "sha512-J27za7sKF6RjhmvSOwOQFeNhNHyP4f4niqPnerJmq73OtLx9Y2PGOhkXOEB0PjhvPJceuttkD2O1yMgEkTGs3Q==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/executor-common": "^0.0.4", - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/executor-common": "^0.0.6", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/disposablestack": "^0.0.6", - "graphql-ws": "^6.0.3", + "graphql-ws": "^6.0.6", "isomorphic-ws": "^5.0.0", "tslib": "^2.8.1", - "ws": "^8.17.1" + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws/node_modules/@graphql-tools/executor-common": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.6.tgz", + "integrity": "sha512-JAH/R1zf77CSkpYATIJw+eOJwsbWocdDjY+avY7G+P5HCXxwQjAjWVkJI1QJBQYjPQDVxwf1fmTZlIN3VOadow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.3.0", + "@graphql-tools/utils": "^10.9.1" }, "engines": { "node": ">=18.0.0" @@ -2817,13 +2784,13 @@ } }, "node_modules/@graphql-tools/executor-legacy-ws": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.17.tgz", - "integrity": "sha512-TvltY6eL4DY1Vt66Z8kt9jVmNcI+WkvVPQZrPbMCM3rv2Jw/sWvSwzUBezRuWX0sIckMifYVh23VPcGBUKX/wg==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.19.tgz", + "integrity": "sha512-bEbv/SlEdhWQD0WZLUX1kOenEdVZk1yYtilrAWjRUgfHRZoEkY9s+oiqOxnth3z68wC2MWYx7ykkS5hhDamixg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", "@types/ws": "^8.0.0", "isomorphic-ws": "^5.0.0", "tslib": "^2.4.0", @@ -2837,14 +2804,14 @@ } }, "node_modules/@graphql-tools/git-loader": { - "version": "8.0.24", - "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.24.tgz", - "integrity": "sha512-ypLC9N2bKNC0QNbrEBTbWKwbV607f7vK2rSGi9uFeGr8E29tWplo6or9V/+TM0ZfIkUsNp/4QX/zKTgo8SbwQg==", + "version": "8.0.26", + "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.26.tgz", + "integrity": "sha512-0g+9eng8DaT4ZmZvUmPgjLTgesUa6M8xrDjNBltRldZkB055rOeUgJiKmL6u8PjzI5VxkkVsn0wtAHXhDI2UXQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.19", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/graphql-tag-pluck": "8.3.21", + "@graphql-tools/utils": "^10.9.1", "is-glob": "4.0.3", "micromatch": "^4.0.8", "tslib": "^2.4.0", @@ -2858,15 +2825,15 @@ } }, "node_modules/@graphql-tools/github-loader": { - "version": "8.0.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.20.tgz", - "integrity": "sha512-Icch8bKZ1iP3zXCB9I0ded1hda9NPskSSalw7ZM21kXvLiOR5nZhdqPF65gCFkIKo+O4NR4Bp51MkKj+wl+vpg==", + "version": "8.0.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.22.tgz", + "integrity": "sha512-uQ4JNcNPsyMkTIgzeSbsoT9hogLjYrZooLUYd173l5eUGUi49EAcsGdiBCKaKfEjanv410FE8hjaHr7fjSRkJw==", "dev": true, "license": "MIT", "dependencies": { "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/graphql-tag-pluck": "^8.3.19", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/graphql-tag-pluck": "^8.3.21", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/fetch": "^0.10.0", "@whatwg-node/promise-helpers": "^1.0.0", "sync-fetch": "0.6.0-2", @@ -2880,14 +2847,14 @@ } }, "node_modules/@graphql-tools/graphql-file-loader": { - "version": "8.0.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.20.tgz", - "integrity": "sha512-inds4My+JJxmg5mxKWYtMIJNRxa7MtX+XIYqqD/nu6G4LzQ5KGaBJg6wEl103KxXli7qNOWeVAUmEjZeYhwNEg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.1.1.tgz", + "integrity": "sha512-5JaUE3zMHW21Oh3bGSNKcr/Mi6oZ9/QWlBCNYbGy+09U23EOZmhPn9a44zP3gXcnnj0C+YVEr8dsMaoaB3UVGQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/import": "7.0.19", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/import": "7.1.1", + "@graphql-tools/utils": "^10.9.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -2900,9 +2867,9 @@ } }, "node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "8.3.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.19.tgz", - "integrity": "sha512-LEw/6IYOUz48HjbWntZXDCzSXsOIM1AyWZrlLoJOrA8QAlhFd8h5Tny7opCypj8FO9VvpPFugWoNDh5InPOEQA==", + "version": "8.3.21", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.21.tgz", + "integrity": "sha512-TJhELNvR1tmghXMi6HVKp/Swxbx1rcSp/zdkuJZT0DCM3vOY11FXY6NW3aoxumcuYDNN3jqXcCPKstYGFPi5GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2911,7 +2878,7 @@ "@babel/plugin-syntax-import-assertions": "^7.26.0", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -2922,13 +2889,14 @@ } }, "node_modules/@graphql-tools/import": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.19.tgz", - "integrity": "sha512-Xtku8G4bxnrr6I3hVf8RrBFGYIbQ1OYVjl7jgcy092aBkNZvy1T6EDmXmYXn5F+oLd9Bks3K3WaMm8gma/nM/Q==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.1.1.tgz", + "integrity": "sha512-zhlhaUmeTfV76vMoLRn9xCVMVc7sLf10ve5GKEhXFFDcWA6+vEZGk9CCm1VlPf2kyKGlF7bwLVzfepb3ZoOU9Q==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", + "@theguild/federation-composition": "^0.19.0", "resolve-from": "5.0.0", "tslib": "^2.4.0" }, @@ -2940,13 +2908,13 @@ } }, "node_modules/@graphql-tools/json-file-loader": { - "version": "8.0.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.18.tgz", - "integrity": "sha512-JjjIxxewgk8HeMR3npR3YbOkB7fxmdgmqB9kZLWdkRKBxrRXVzhryyq+mhmI0Evzt6pNoHIc3vqwmSctG2sddg==", + "version": "8.0.20", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.20.tgz", + "integrity": "sha512-5v6W+ZLBBML5SgntuBDLsYoqUvwfNboAwL6BwPHi3z/hH1f8BS9/0+MCW9OGY712g7E4pc3y9KqS67mWF753eA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -2959,14 +2927,14 @@ } }, "node_modules/@graphql-tools/load": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.0.tgz", - "integrity": "sha512-OGfOm09VyXdNGJS/rLqZ6ztCiG2g6AMxhwtET8GZXTbnjptFc17GtKwJ3Jv5w7mjJ8dn0BHydvIuEKEUK4ciYw==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.2.tgz", + "integrity": "sha512-WhDPv25/jRND+0uripofMX0IEwo6mrv+tJg6HifRmDu8USCD7nZhufT0PP7lIcuutqjIQFyogqT70BQsy6wOgw==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/schema": "^10.0.23", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", "p-limit": "3.1.0", "tslib": "^2.4.0" }, @@ -2978,13 +2946,13 @@ } }, "node_modules/@graphql-tools/merge": { - "version": "9.0.24", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.24.tgz", - "integrity": "sha512-NzWx/Afl/1qHT3Nm1bghGG2l4jub28AdvtG11PoUlmjcIjnFBJMv4vqL0qnxWe8A82peWo4/TkVdjJRLXwgGEw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.1.tgz", + "integrity": "sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -3010,46 +2978,15 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/prisma-loader": { - "version": "8.0.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-8.0.17.tgz", - "integrity": "sha512-fnuTLeQhqRbA156pAyzJYN0KxCjKYRU5bz1q/SKOwElSnAU4k7/G1kyVsWLh7fneY78LoMNH5n+KlFV8iQlnyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/url-loader": "^8.0.15", - "@graphql-tools/utils": "^10.5.6", - "@types/js-yaml": "^4.0.0", - "@whatwg-node/fetch": "^0.10.0", - "chalk": "^4.1.0", - "debug": "^4.3.1", - "dotenv": "^16.0.0", - "graphql-request": "^6.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "jose": "^5.0.0", - "js-yaml": "^4.0.0", - "lodash": "^4.17.20", - "scuid": "^1.1.0", - "tslib": "^2.4.0", - "yaml-ast-parser": "^0.0.43" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.19.tgz", - "integrity": "sha512-xnjLpfzw63yIX1bo+BVh4j1attSwqEkUbpJ+HAhdiSUa3FOQFfpWgijRju+3i87CwhjBANqdTZbcsqLT1hEXig==", + "version": "7.0.21", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.21.tgz", + "integrity": "sha512-vMdU0+XfeBh9RCwPqRsr3A05hPA3MsahFn/7OAwXzMySA5EVnSH5R4poWNs3h1a0yT0tDPLhxORhK7qJdSWj2A==", "dev": true, "license": "MIT", "dependencies": { "@ardatan/relay-compiler": "^12.0.3", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -3060,14 +2997,14 @@ } }, "node_modules/@graphql-tools/schema": { - "version": "10.0.23", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.23.tgz", - "integrity": "sha512-aEGVpd1PCuGEwqTXCStpEkmheTHNdMayiIKH1xDWqYp9i8yKv9FRDgkGrY4RD8TNxnf7iII+6KOBGaJ3ygH95A==", + "version": "10.0.25", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.25.tgz", + "integrity": "sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/merge": "^9.0.24", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/merge": "^9.1.1", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -3078,16 +3015,16 @@ } }, "node_modules/@graphql-tools/url-loader": { - "version": "8.0.31", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.31.tgz", - "integrity": "sha512-QGP3py6DAdKERHO5D38Oi+6j+v0O3rkBbnLpyOo87rmIRbwE6sOkL5JeHegHs7EEJ279fBX6lMt8ry0wBMGtyA==", + "version": "8.0.33", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.33.tgz", + "integrity": "sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw==", "dev": true, "license": "MIT", "dependencies": { "@graphql-tools/executor-graphql-ws": "^2.0.1", "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/executor-legacy-ws": "^1.1.17", - "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/executor-legacy-ws": "^1.1.19", + "@graphql-tools/utils": "^10.9.1", "@graphql-tools/wrap": "^10.0.16", "@types/ws": "^8.0.0", "@whatwg-node/fetch": "^0.10.0", @@ -3105,9 +3042,9 @@ } }, "node_modules/@graphql-tools/utils": { - "version": "10.8.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.8.6.tgz", - "integrity": "sha512-Alc9Vyg0oOsGhRapfL3xvqh1zV8nKoFUdtLhXX7Ki4nClaIJXckrA86j+uxEuG3ic6j4jlM1nvcWXRn/71AVLQ==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.1.tgz", + "integrity": "sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw==", "dev": true, "license": "MIT", "dependencies": { @@ -3125,15 +3062,15 @@ } }, "node_modules/@graphql-tools/wrap": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.0.tgz", - "integrity": "sha512-M7QolM/cJwM2PNAJS1vphT2/PDVSKtmg5m+fxHrFfKpp2RRosJSvYPzUD/PVPqF2rXTtnCwkgh1s5KIsOPCz+w==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.4.tgz", + "integrity": "sha512-7pyNKqXProRjlSdqOtrbnFRMQAVamCmEREilOXtZujxY6kYit3tvWWSjUrcIOheltTffoRh7EQSjpy2JDCzasg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/delegate": "^10.2.19", - "@graphql-tools/schema": "^10.0.11", - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/delegate": "^10.2.23", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", "tslib": "^2.8.1" }, @@ -3154,28 +3091,50 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@gulpjs/to-absolute-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", - "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.0.tgz", + "integrity": "sha512-5+Q3PKH35YsnoPTh75LucALdAxom6xh5D1oeY561x4cqBuH24ZFVyFREPe14xgnrtmGu3EEt1dIi60wRVSnGCw==", "dev": true, "license": "MIT", "dependencies": { - "is-negated-glob": "^1.0.0" + "@inquirer/ansi": "^1.0.1", + "@inquirer/core": "^10.3.0", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" }, "engines": { - "node": ">=10.13.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/confirm": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz", - "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==", + "version": "5.1.19", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", + "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.13", - "@inquirer/type": "^3.0.7" + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" }, "engines": { "node": ">=18" @@ -3190,15 +3149,15 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.13", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", - "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -3217,57 +3176,274 @@ } } }, - "node_modules/@inquirer/core/node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@inquirer/core/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.0.tgz", - "integrity": "sha512-5v3YXc5ZMfL6OJqXPrX9csb4l7NlQA2doO1yynUjpUChT9hg4JcuBVP0RbsEJ/3SL/sxWEyFjT2W69ZhtoBWqg==", + "node_modules/@inquirer/editor": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.21.tgz", + "integrity": "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.6.3" + "@inquirer/core": "^10.3.0", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.9" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.21.tgz", + "integrity": "sha512-+mScLhIcbPFmuvU3tAGBed78XvYHSvCl6dBiYMlzCLhpr0bzGzd8tfivMMeqND6XZiaZ1tgusbUHJEfc6YzOdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/@inquirer/input": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.5.tgz", + "integrity": "sha512-7GoWev7P6s7t0oJbenH0eQ0ThNdDJbEAEtVt9vsrYZ9FulIokvd823yLyhQlWHJPGce1wzP53ttfdCZmonMHyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.21.tgz", + "integrity": "sha512-5QWs0KGaNMlhbdhOSCFfKsW+/dcAVC2g4wT/z2MCiZM47uLgatC5N20kpkDQf7dHx+XFct/MJvvNGy6aYJn4Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.21", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.21.tgz", + "integrity": "sha512-xxeW1V5SbNFNig2pLfetsDb0svWlKuhmr7MPJZMYuDnCTkpVBI+X/doudg4pznc1/U+yYmWFFOi4hNvGgUo7EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.9.0.tgz", + "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.0", + "@inquirer/confirm": "^5.1.19", + "@inquirer/editor": "^4.2.21", + "@inquirer/expand": "^4.0.21", + "@inquirer/input": "^4.2.5", + "@inquirer/number": "^3.0.21", + "@inquirer/password": "^4.0.21", + "@inquirer/rawlist": "^4.1.9", + "@inquirer/search": "^3.2.0", + "@inquirer/select": "^4.4.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.9.tgz", + "integrity": "sha512-AWpxB7MuJrRiSfTKGJ7Y68imYt8P9N3Gaa7ySdkFj1iWjr6WfbGAhdZvw/UnhFXTHITJzxGUI9k8IX7akAEBCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.0.tgz", + "integrity": "sha512-a5SzB/qrXafDX1Z4AZW3CsVoiNxcIYCzYP7r9RzrfMpaLpB+yWi5U8BWagZyLmwR0pKbbL5umnGRd0RzGVI8bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.0.tgz", + "integrity": "sha512-kaC3FHsJZvVyIjYBs5Ih8y8Bj4P/QItQWrZW22WJax7zTN+ZPXVGuOM55vzbdCP9zKUiBd9iEJVdesujfF+cAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/core": "^10.3.0", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/type": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", - "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", "dev": true, "license": "MIT", "engines": { @@ -3282,6 +3458,29 @@ } } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3301,9 +3500,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -3314,9 +3513,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -3352,9 +3551,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -3400,6 +3599,7 @@ "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz", "integrity": "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==", "dev": true, + "license": "MIT", "dependencies": { "glob": "^10.0.0", "magic-string": "^0.30.0", @@ -3416,15 +3616,27 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -3436,26 +3648,27 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@mdx-js/react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", - "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "dev": true, "license": "MIT", "dependencies": { @@ -3471,9 +3684,9 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.39.2", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.2.tgz", - "integrity": "sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", + "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3489,16 +3702,16 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", - "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -3539,178 +3752,6 @@ "node": ">= 8" } }, - "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/core": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", - "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/endpoint": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/graphql": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", - "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/request": "^8.4.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", - "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^12.6.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "5" - } - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^20.0.0" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", - "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^12.6.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "5" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^20.0.0" - } - }, - "node_modules/@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^24.2.0" - } - }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -3736,10 +3777,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.12.0.tgz", + "integrity": "sha512-/IfGWLNdmS1kVYM2g+Xw4qXNWtCPZ/i5YMprflA8FC3vAjT4N0VucQcDxUPHxatQwre4qnhbFFWqRa1mz6Cgkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.12.0.tgz", + "integrity": "sha512-H3Ehyinfx2VO8F5TwdaD/WY686Ia6J1H3LP0tgpNjlPGH2TrTniPERiwjqtOm/xHEef0KJvb/yfmUKLbHudhCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.2.0.tgz", - "integrity": "sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.12.0.tgz", + "integrity": "sha512-hmm+A/0WdEtIeBrPtUHoSTzJefrZkhGSrmv5pwELKiqNqd+/gctzmTlt6wWrU8BMIryDMT9fWqLSQ3+NYfqAEA==", "cpu": [ "arm64" ], @@ -3751,9 +3820,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.2.0.tgz", - "integrity": "sha512-0zhgNUm5bYezdSFOg3FYhtVP83bAq7FTV/3suGQDl/43MixfQG7+bl+hlrP4mz6WlD2SUb2u9BomnJWl1uey9w==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.12.0.tgz", + "integrity": "sha512-g1tVu53EMfuRKs67o0PZR0+y/WXl/Tfn3d2ggjK3Hj17pQPcb9x1+Y6W7n4EjIDttwLZbCPCEr06X+aC03I45A==", "cpu": [ "x64" ], @@ -3765,9 +3834,9 @@ ] }, "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.2.0.tgz", - "integrity": "sha512-SHOxfCcZV1axeIGfyeD1BkdLvfQgjmPy18tO0OUXGElcdScxD6MqU5rj/AVtiuBT+51GtFfOKlwl1+BdVwhD1A==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.12.0.tgz", + "integrity": "sha512-TiMatzvcVMSOiAx8sbnAw7UCfQpZDlm91ItywZrSHlQIJqDBipOmjIEYUMc2p823Y+fJ2ADL5UBjUB2kfqpedw==", "cpu": [ "x64" ], @@ -3779,9 +3848,23 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.2.0.tgz", - "integrity": "sha512-mgEkYrJ+N90sgEDqEZ07zH+4I1D28WjqAhdzfW3aS2x2vynVpoY9jWfHuH8S62vZt3uATJrTKTRa8CjPWEsrdw==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.12.0.tgz", + "integrity": "sha512-zU+9UgxPIvfReqmRr/dqZt3387HPgcH0hA4u0QGE+280EFjBYYL2rxGDxK0L+keO6vc2+ITWVDXm9KIj+alofg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.12.0.tgz", + "integrity": "sha512-dfO1rrOeELYWD/BewMCp81k1I3pOdtAi2VCKg/A1I8z0uI4OR6cThb5dV9fpHkj7zlb0Y5iZFPe+NTbI/u1MgQ==", "cpu": [ "arm" ], @@ -3793,9 +3876,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.2.0.tgz", - "integrity": "sha512-BhEzNLjn4HjP8+Q18D3/jeIDBxW7OgoJYIjw2CaaysnYneoTlij8hPTKxHfyqq4IGM3fFs9TLR/k338M3zkQ7g==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.12.0.tgz", + "integrity": "sha512-JJNyN1ueryETKTUsG57+u0GDbtHKVcwcUoC6YyJmDdWE0o/3twXtHuS+F/121a2sVK8PKlROqGAev+STx3AuuQ==", "cpu": [ "arm64" ], @@ -3807,9 +3890,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.2.0.tgz", - "integrity": "sha512-yxbMYUgRmN2V8x8XoxmD/Qq6aG7YIW3ToMDILfmcfeeRRVieEJ3DOWBT0JSE+YgrOy79OyFDH/1lO8VnqLmDQQ==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.12.0.tgz", + "integrity": "sha512-rQHoxL0H0WwYUuukPUscLyzWwTl/hyogptYsY+Ye6AggJEOuvgJxMum2glY7etGIGOXxrfjareHnNO1tNY7WYg==", "cpu": [ "arm64" ], @@ -3820,10 +3903,38 @@ "linux" ] }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.12.0.tgz", + "integrity": "sha512-XPUZSctO+FrC0314Tcth+GrTtzy2yaYqyl8weBMAbKFMwuV8VnR2SHg9dmtI9vkukmM3auOLj0Kqjpl3YXwXiw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.2.0.tgz", - "integrity": "sha512-QG1UfgC2N2qhW1tOnDCgB/26vn1RCshR5sYPhMeaxO1gMQ3kEKbZ3QyBXxrG1IX5qsXYj5hPDJLDYNYUjRcOpg==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.12.0.tgz", + "integrity": "sha512-AmMjcP+6zHLF1JNq/p3yPEcXmZW/Xw5Xl19Zd0eBCSyGORJRuUOkcnyC8bwMO43b/G7PtausB83fclnFL5KZ3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.12.0.tgz", + "integrity": "sha512-K2/yFBqFQOKyVwQxYDAKqDtk2kS4g58aGyj/R1bvYPr2P7v7971aUG/5m2WD5u2zSqWBfu1o4PdhX0lsqvA3vQ==", "cpu": [ "riscv64" ], @@ -3835,9 +3946,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.2.0.tgz", - "integrity": "sha512-uqTDsQdi6mrkSV1gvwbuT8jf/WFl6qVDVjNlx7IPSaAByrNiJfPrhTmH8b+Do58Dylz7QIRZgxQ8CHIZSyBUdg==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.12.0.tgz", + "integrity": "sha512-uSl4jo78tONGZtwsOA4ldT/OI7/hoHJhSMlGYE4Z/lzwMjkAaBdX4soAK5P/rL+U2yCJlRMnnoUckhXlZvDbSw==", "cpu": [ "s390x" ], @@ -3849,9 +3960,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.2.0.tgz", - "integrity": "sha512-GZdHXhJ7p6GaQg9MjRqLebwBf8BLvGIagccI6z5yMj4fV3LU4QuDfwSEERG+R6oQ/Su9672MBqWwncvKcKT68w==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.12.0.tgz", + "integrity": "sha512-YjL8VAkbPyQ1kUuR6pOBk1O+EkxOoLROTa+ia1/AmFLuXYNltLGI1YxOY14i80cKpOf0Z59IXnlrY3coAI9NDQ==", "cpu": [ "x64" ], @@ -3863,9 +3974,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.2.0.tgz", - "integrity": "sha512-YBAC3GOicYznReG2twE7oFPSeK9Z1f507z1EYWKg6HpGYRYRlJyszViu7PrhMT85r/MumDTs429zm+CNqpFWOA==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.12.0.tgz", + "integrity": "sha512-qpHPU0qqeJXh7cPzA+I+WWA6RxtRArfmSrhTXidbiQ08G5A1e55YQwExWkitB2rSqN6YFxnpfhHKo9hyhpyfSg==", "cpu": [ "x64" ], @@ -3877,9 +3988,9 @@ ] }, "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.2.0.tgz", - "integrity": "sha512-+qlIg45CPVPy+Jn3vqU1zkxA/AAv6e/2Ax/ImX8usZa8Tr2JmQn/93bmSOOOnr9fXRV9d0n4JyqYzSWxWPYDEw==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.12.0.tgz", + "integrity": "sha512-oqg80bERZAagWLqYmngnesE0/2miv4lST7+wiiZniD6gyb1SoRckwEkbTsytGutkudFtw7O61Pon6pNlOvyFaA==", "cpu": [ "wasm32" ], @@ -3887,16 +3998,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@napi-rs/wasm-runtime": "^1.0.7" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.2.0.tgz", - "integrity": "sha512-AI4KIpS8Zf6vwfOPk0uQPSC0pQ1m5HU4hCbtrgL21JgJSlnJaeEu3/aoOBB45AXKiExBU9R+CDR7aSnW7uhc5A==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.12.0.tgz", + "integrity": "sha512-qKH816ycEN9yR/TX91CP1/i6xyVNHKX9VEOYa3XzQROPVtcYG2F6A3ng/PhwpJvS1cmL/DlilhglZe9KWkhNjg==", "cpu": [ "arm64" ], @@ -3907,10 +4018,24 @@ "win32" ] }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.12.0.tgz", + "integrity": "sha512-3bgxubTlhzF6BwBnhGz5BTboarl1upuanEr6i0dncjfEcU+Z9xAOgbtA7Ip3G3EKDjE1objRKK+ny8PKJZ3b7Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.2.0.tgz", - "integrity": "sha512-r19cQc7HaEJ76HFsMsbiKMTIV2YqFGSof8H5hB7e5Jkb/23Y8Isv1YrSzkDaGhcw02I/COsrPo+eEmjy35eFuA==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.12.0.tgz", + "integrity": "sha512-rbiWYQWxwy+x7+KgNAoAGYIPB3xUclQlFVV3L5lwfsbp4PQPomJohHowlWgi3GRAEybM5+ZL9xny0YfpJOsthA==", "cpu": [ "x64" ], @@ -3933,9 +4058,9 @@ } }, "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { @@ -3962,16 +4087,16 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", - "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4048,14 +4173,14 @@ } }, "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz", - "integrity": "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==", + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" @@ -4076,20 +4201,20 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", - "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", @@ -4127,12 +4252,12 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", @@ -4154,16 +4279,16 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", - "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -4183,9 +4308,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4223,12 +4348,12 @@ } }, "node_modules/@radix-ui/react-form": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.7.tgz", - "integrity": "sha512-IXLKFnaYvFg/KkeV5QfOX7tRnwHXp127koOFUjLWMTrRv5Rny3DQcAtIFFeA/Cli4HHM8DuJCXAUsgnFVJndlw==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", @@ -4292,25 +4417,25 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", @@ -4332,9 +4457,9 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -4388,9 +4513,9 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -4459,12 +4584,12 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -4665,16 +4790,16 @@ "license": "MIT" }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "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==", "dev": true, "license": "MIT" }, "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4695,9 +4820,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", - "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", "cpu": [ "arm" ], @@ -4709,9 +4834,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", - "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", "cpu": [ "arm64" ], @@ -4723,9 +4848,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", - "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", "cpu": [ "arm64" ], @@ -4737,9 +4862,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", - "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", "cpu": [ "x64" ], @@ -4751,9 +4876,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", - "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", "cpu": [ "arm64" ], @@ -4765,9 +4890,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", - "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", "cpu": [ "x64" ], @@ -4779,9 +4904,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", - "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", "cpu": [ "arm" ], @@ -4793,9 +4918,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", - "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", "cpu": [ "arm" ], @@ -4807,9 +4932,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", - "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", "cpu": [ "arm64" ], @@ -4821,9 +4946,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", - "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", "cpu": [ "arm64" ], @@ -4835,9 +4960,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", - "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", "cpu": [ "loong64" ], @@ -4848,10 +4973,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", - "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", "cpu": [ "ppc64" ], @@ -4863,9 +4988,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", - "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", "cpu": [ "riscv64" ], @@ -4877,9 +5002,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", - "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", "cpu": [ "riscv64" ], @@ -4891,9 +5016,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", - "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", "cpu": [ "s390x" ], @@ -4905,9 +5030,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", - "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", "cpu": [ "x64" ], @@ -4919,9 +5044,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", - "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", "cpu": [ "x64" ], @@ -4932,10 +5057,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", - "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", "cpu": [ "arm64" ], @@ -4947,9 +5086,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", - "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", "cpu": [ "ia32" ], @@ -4961,9 +5100,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", - "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", "cpu": [ "x64" ], @@ -4981,17 +5120,37 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@storybook/addon-docs": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.3.tgz", - "integrity": "sha512-iCzuHRyUgir2+ExqPO4ouxm90zW+6dkNuB4lyyFwNU10slJhVT8yGPk8PVOT6LhXMIii+7Hqc4dB0tj+kLOW/A==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.0.8.tgz", + "integrity": "sha512-PYuaGXGycsamK/7OrFoE4syHGy22mdqqArl67cfosRwmRxZEI9ManQK0jTjNQM9ZX14NpThMOSWNGoWLckkxog==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "9.1.3", - "@storybook/icons": "^1.4.0", - "@storybook/react-dom-shim": "9.1.3", + "@storybook/csf-plugin": "10.0.8", + "@storybook/icons": "^1.6.0", + "@storybook/react-dom-shim": "10.0.8", "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" @@ -5001,17 +5160,17 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.3" + "storybook": "^10.0.8" } }, "node_modules/@storybook/builder-vite": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.3.tgz", - "integrity": "sha512-bstS/GsVJ5zVkRKAJociocA2omxU4CaNAP58fxS280JiRYgcrRaydDd7vwk6iGJ3xWbzwV0wH8SP54LVNyRY6Q==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "9.1.3", + "@storybook/csf-plugin": "10.0.8", "ts-dedent": "^2.0.0" }, "funding": { @@ -5019,25 +5178,43 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.3", + "storybook": "^10.0.8", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.3.tgz", - "integrity": "sha512-wqh+tTCX2WZqVDVjhk/a6upsyYj/Kc85Wf6ywPx4pcFYxQZxiKF/wtuM9yzEpZC6fZHNvlKkzXWvP4wJOnm+zg==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.0.8.tgz", + "integrity": "sha512-OtLUWHIm3SDGtclQn6Mdd/YsWizLBgdEBRAdekGtwI/TvICfT7gpWYIycP53v2t9ufu2MIXjsxtV2maZKs8sZg==", "dev": true, "license": "MIT", "dependencies": { - "unplugin": "^1.3.1" + "unplugin": "^2.3.5" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.3" + "esbuild": "*", + "rollup": "*", + "storybook": "^10.0.8", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "esbuild": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/@storybook/global": { @@ -5048,9 +5225,9 @@ "license": "MIT" }, "node_modules/@storybook/icons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.4.0.tgz", - "integrity": "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.6.0.tgz", + "integrity": "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==", "dev": true, "license": "MIT", "engines": { @@ -5062,26 +5239,23 @@ } }, "node_modules/@storybook/react": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.3.tgz", - "integrity": "sha512-CgJMk4Y8EfoFxWiTB53QxnN+nQbAkw+NBaNjsaaeDNOE1R0ximP/fn5b2jcLvM+b5ojjJiJL1QCzFyoPWImHIQ==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.0.8.tgz", + "integrity": "sha512-PkuPb8sAqmjjkowSzm3rutiSuETvZI2F8SnjbHE6FRqZWWK4iFoaUrQbrg5kpPAtX//xIrqkdFwlbmQ3skhiPA==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "9.1.3" - }, - "engines": { - "node": ">=20.0.0" + "@storybook/react-dom-shim": "10.0.8" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "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", - "storybook": "^9.1.3", + "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", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -5091,9 +5265,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.3.tgz", - "integrity": "sha512-zIgFwZqV8cvE+lzJDcD13rItxoWyYNUWu7eJQAnHz5RnyHhpu6rFgQej7i6J3rPmy9xVe+Rq6XsXgDNs6pIekQ==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.0.8.tgz", + "integrity": "sha512-ojuH22MB9Sz6rWbhTmC5IErZr0ZADbZijtPteUdydezY7scORT00UtbNoBcG0V6iVjdChgDtSKw2KHUUfchKqg==", "dev": true, "license": "MIT", "funding": { @@ -5101,42 +5275,265 @@ "url": "https://opencollective.com/storybook" }, "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", - "storybook": "^9.1.3" + "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" } }, "node_modules/@storybook/react-vite": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.3.tgz", - "integrity": "sha512-iNRRxA5G9Yaw5etbRdCMnJtjI1VkzA7juc+/caVhKKut25sI8cOF4GRPLCbotLz9xmitQR2X7beZMPPVIYk86A==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.0.8.tgz", + "integrity": "sha512-HS2X4qlitrZr3/sN2+ollxAaNE813IasZRE8lOez1Ey1ISGBtYIb9rmJs82MK35+yDM0pHdiDjkFMD4SkNYh2g==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "9.1.3", - "@storybook/react": "9.1.3", - "find-up": "^7.0.0", + "@storybook/builder-vite": "10.0.8", + "@storybook/react": "10.0.8", + "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, - "engines": { - "node": ">=20.0.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "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", - "storybook": "^9.1.3", + "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", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@swc/core": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.19.tgz", + "integrity": "sha512-V1r4wFdjaZIUIZZrV2Mb/prEeu03xvSm6oatPxsvnXKF9lNh5Jtk9QvUdiVfD9rrvi7bXrAVhg9Wpbmv/2Fl1g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.19", + "@swc/core-darwin-x64": "1.13.19", + "@swc/core-linux-arm-gnueabihf": "1.13.19", + "@swc/core-linux-arm64-gnu": "1.13.19", + "@swc/core-linux-arm64-musl": "1.13.19", + "@swc/core-linux-x64-gnu": "1.13.19", + "@swc/core-linux-x64-musl": "1.13.19", + "@swc/core-win32-arm64-msvc": "1.13.19", + "@swc/core-win32-ia32-msvc": "1.13.19", + "@swc/core-win32-x64-msvc": "1.13.19" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.19.tgz", + "integrity": "sha512-NxDyte9tCJSJ8+R62WDtqwg8eI57lubD52sHyGOfezpJBOPr36bUSGGLyO3Vod9zTGlOu2CpkuzA/2iVw92u1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.19.tgz", + "integrity": "sha512-+w5DYrJndSygFFRDcuPYmx5BljD6oYnAohZ15K1L6SfORHp/BTSIbgSFRKPoyhjuIkDiq3W0um8RoMTOBAcQjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.19.tgz", + "integrity": "sha512-7LlfgpdwwYq2q7himNkAAFo4q6jysMLFNoBH6GRP7WL29NcSsl5mPMJjmYZymK+sYq/9MTVieDTQvChzYDsapw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.19.tgz", + "integrity": "sha512-ml3I6Lm2marAQ3UC/TS9t/yILBh/eDSVHAdPpikp652xouWAVW1znUeV6bBSxe1sSZIenv+p55ubKAWq/u84sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.19.tgz", + "integrity": "sha512-M/otFc3/rWWkbF6VgbOXVzUKVoE7MFcphTaStxJp4bwb7oP5slYlxMZN51Dk/OTOfvCDo9pTAFDKNyixbkXMDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.19.tgz", + "integrity": "sha512-NoMUKaOJEdouU4tKF88ggdDHFiRRING+gYLxDqnTfm+sUXaizB5OGBRzvSVDYSXQb1SuUuChnXFPFzwTWbt3ZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.19.tgz", + "integrity": "sha512-r6krlZwyu8SBaw24QuS1lau2I9q8M+eJV6ITz0rpb6P1Bx0elf9ii5Bhh8ddmIqXXH8kOGSjC/dwcdHbZqAhgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.19.tgz", + "integrity": "sha512-awcZSIuxyVn0Dw28VjMvgk1qiDJ6CeQwHkZNUjg2UxVlq23zE01NMMp+zkoGFypmLG9gaGmJSzuoqvk/WCQ5tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.19.tgz", + "integrity": "sha512-H5d+KO7ISoLNgYvTbOcCQjJZNM3R7yaYlrMAF13lUr6GSiOUX+92xtM31B+HvzAWI7HtvVe74d29aC1b1TpXFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.19", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.19.tgz", + "integrity": "sha512-qNoyCpXvv2O3JqXKanRIeoMn03Fho/As+N4Fhe7u0FsYh4VYqGQah4DGDzEP/yjl4Gx1IElhqLGDhCCGMwWaDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tanstack/history": { "version": "1.131.2", "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.131.2.tgz", @@ -5151,9 +5548,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.85.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", - "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==", + "version": "5.90.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.7.tgz", + "integrity": "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==", "license": "MIT", "funding": { "type": "github", @@ -5161,9 +5558,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.84.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz", - "integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==", + "version": "5.90.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz", + "integrity": "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==", "dev": true, "license": "MIT", "funding": { @@ -5172,12 +5569,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.85.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", - "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", + "version": "5.90.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.7.tgz", + "integrity": "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.85.5" + "@tanstack/query-core": "5.90.7" }, "funding": { "type": "github", @@ -5188,32 +5585,32 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.85.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.85.5.tgz", - "integrity": "sha512-6Ol6Q+LxrCZlQR4NoI5181r+ptTwnlPG2t7H9Sp3klxTBhYGunONqcgBn2YKRPsaKiYM8pItpKMdMXMEINntMQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.84.0" + "@tanstack/query-devtools": "5.90.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.85.5", + "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "node_modules/@tanstack/react-router": { - "version": "1.131.27", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.131.27.tgz", - "integrity": "sha512-JLUsmlarNxMz7VDhFscZCqoc2quhocQZKhia/7YXWf8Jbc8rANk6lukK4ecYn92m/ytoHAAy77JeaB6n0HvqwQ==", + "version": "1.131.44", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.131.44.tgz", + "integrity": "sha512-LREJfrl8lSedXHCRAAt0HvnHFP9ikAQWnVhYRM++B26w4ZYQBbLvgCT1BCDZVY7MR6rslcd4OfgpZMOyVhNzFg==", "license": "MIT", "dependencies": { "@tanstack/history": "1.131.2", "@tanstack/react-store": "^0.7.0", - "@tanstack/router-core": "1.131.27", + "@tanstack/router-core": "1.131.44", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" @@ -5231,13 +5628,13 @@ } }, "node_modules/@tanstack/react-router-devtools": { - "version": "1.131.27", - "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.131.27.tgz", - "integrity": "sha512-SHulN0a7hZvyl3fXi+VLHxdMKdsg1lhPOZeKd5xs6bu/x+N5FaXEA5bUPGB2sbiSYXw/XFcjUqR5dkw8T1dkXg==", + "version": "1.131.44", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.131.44.tgz", + "integrity": "sha512-JGICSLe3ZIqayo2Pz9bpCBLrK8NIruYSQoe/JkZimSGltV3HU+uPb1dohw0CpyxVuhx+tDqFBzq4cDPCABs4/w==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/router-devtools-core": "1.131.27" + "@tanstack/router-devtools-core": "1.131.44" }, "engines": { "node": ">=12" @@ -5247,15 +5644,15 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-router": "^1.131.27", + "@tanstack/react-router": "^1.131.44", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "node_modules/@tanstack/react-router-devtools/node_modules/@tanstack/router-devtools-core": { - "version": "1.131.27", - "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.131.27.tgz", - "integrity": "sha512-upoMv/uq1CQdrOyBO2h6CLXI1Ym7Rawoovt26fN1Wl+RMXqKGVpHAXYuKpugdFMFhFieccKVYcrj9NP4V5BIDw==", + "version": "1.131.44", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.131.44.tgz", + "integrity": "sha512-ZpQfRERLAjZ2NBdFOWjlrbMzQ+23aGs+9324KVdLzZkcd1lc0ztpLb5HAGtqLXfncvO60TfiRz106ygjKsaJow==", "dev": true, "license": "MIT", "dependencies": { @@ -5271,7 +5668,7 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/router-core": "^1.131.27", + "@tanstack/router-core": "^1.131.44", "csstype": "^3.0.10", "solid-js": ">=1.9.5", "tiny-invariant": "^1.3.3" @@ -5283,12 +5680,12 @@ } }, "node_modules/@tanstack/react-store": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.1.tgz", - "integrity": "sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.5.tgz", + "integrity": "sha512-A+WZtEnHZpvbKXm8qR+xndNKywBLez2KKKKEQc7w0Qs45GvY1LpRI3BTZNmELwEVim8+Apf99iEDH2J+MUIzlQ==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.7.1", + "@tanstack/store": "0.7.5", "use-sync-external-store": "^1.5.0" }, "funding": { @@ -5301,9 +5698,9 @@ } }, "node_modules/@tanstack/router-core": { - "version": "1.131.27", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.27.tgz", - "integrity": "sha512-NEBNxZ/LIBIh6kvQntr6bKq57tDe55zecyTtjAmzPkYFsMy1LXEpRm5H3BPiteBMRApAjuaq+bS1qA664hLH6Q==", + "version": "1.131.44", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.44.tgz", + "integrity": "sha512-Npi9xB3GSYZhRW8+gPhP6bEbyx0vNc8ZNwsi0JapdiFpIiszgRJ57pesy/rklruv46gYQjLVA5KDOsuaCT/urA==", "license": "MIT", "dependencies": { "@tanstack/history": "1.131.2", @@ -5323,13 +5720,13 @@ } }, "node_modules/@tanstack/router-generator": { - "version": "1.131.27", - "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.131.27.tgz", - "integrity": "sha512-PXBIVl45q2bBq9g0DDXLBGeKjO9eExcZd2JotLjLdIJ0I/wdxPQOBJHLPZfnmbf3vispToedRvG3b1YDWjL48g==", + "version": "1.131.44", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.131.44.tgz", + "integrity": "sha512-CnrlRkGatdQXdvTteflOTMANupb1z59CO3DSV+UzBkTG+g+vfWgJeKQ0EkfwZ2QuS6Su2v5r5EMHs/AookeZZw==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/router-core": "1.131.27", + "@tanstack/router-core": "1.131.44", "@tanstack/router-utils": "1.131.2", "@tanstack/virtual-file-routes": "1.131.2", "prettier": "^3.5.0", @@ -5347,9 +5744,9 @@ } }, "node_modules/@tanstack/router-plugin": { - "version": "1.131.27", - "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.131.27.tgz", - "integrity": "sha512-0V611ehOE8nfCFT2tvrLfQMroyoYW/virDXPaaFe38hdDxslmfCW2miJxngxz4+QqgK/M3sX71ElrZDvkP2Ixw==", + "version": "1.131.44", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.131.44.tgz", + "integrity": "sha512-CvheUPlB8vxXf23RSDz6q97l1EI5H3f+1qJ/LEBvy7bhls8vYouJ3xyTeu4faz8bEEieLUoVQrCcr+xFY0lkuw==", "dev": true, "license": "MIT", "dependencies": { @@ -5359,8 +5756,8 @@ "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", - "@tanstack/router-core": "1.131.27", - "@tanstack/router-generator": "1.131.27", + "@tanstack/router-core": "1.131.44", + "@tanstack/router-generator": "1.131.44", "@tanstack/router-utils": "1.131.2", "@tanstack/virtual-file-routes": "1.131.2", "babel-dead-code-elimination": "^1.0.10", @@ -5377,7 +5774,7 @@ }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", - "@tanstack/react-router": "^1.131.27", + "@tanstack/react-router": "^1.131.44", "vite": ">=5.0.0 || >=6.0.0", "vite-plugin-solid": "^2.11.2", "webpack": ">=5.92.0" @@ -5400,21 +5797,6 @@ } } }, - "node_modules/@tanstack/router-plugin/node_modules/unplugin": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz", - "integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.1", - "picomatch": "^4.0.2", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, "node_modules/@tanstack/router-utils": { "version": "1.131.2", "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.131.2.tgz", @@ -5438,9 +5820,9 @@ } }, "node_modules/@tanstack/store": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.1.tgz", - "integrity": "sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.5.tgz", + "integrity": "sha512-qd/OjkjaFRKqKU4Yjipaen/EOB9MyEg6Wr9fW103RBPACf1ZcKhbhcu2S5mj5IgdPib6xFIgCUti/mKVkl+fRw==", "license": "MIT", "funding": { "type": "github", @@ -5462,9 +5844,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "peer": true, @@ -5473,9 +5855,9 @@ "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", - "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { @@ -5483,9 +5865,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", - "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", "dependencies": { @@ -5551,10 +5933,29 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@theguild/federation-composition": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@theguild/federation-composition/-/federation-composition-0.19.1.tgz", + "integrity": "sha512-E4kllHSRYh+FsY0VR+fwl0rmWhDV8xUgWawLZTXmy15nCWQwj0BDsoEpdEXjPh7xes+75cRaeJcSbZ4jkBuSdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "constant-case": "^3.0.4", + "debug": "4.4.1", + "json5": "^2.2.3", + "lodash.sortby": "^4.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "graphql": "^16.0.0" + } + }, "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -5606,13 +6007,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/chai": { @@ -5625,13 +6026,6 @@ "@types/deep-eql": "*" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -5653,13 +6047,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -5667,27 +6054,20 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/react": { - "version": "19.1.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", - "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5695,13 +6075,13 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", - "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/resolve": { @@ -5725,13 +6105,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/symlink-or-copy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz", - "integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -5750,9 +6123,9 @@ } }, "node_modules/@vector-im/compound-design-tokens": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@vector-im/compound-design-tokens/-/compound-design-tokens-5.0.2.tgz", - "integrity": "sha512-LcdrGY9qktuSs9TNX+DdGGq64vP7Qk5FiiqtZBr4PEk+hCQPEyRtKDfkXbAST+0tpAjUqVp5pzlOqNUKhpIhfg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@vector-im/compound-design-tokens/-/compound-design-tokens-6.0.0.tgz", + "integrity": "sha512-Jk0NsLPCvdcuZi6an1cfyf4MDcIuoPlvja5ZWgJcORyGQZV1eLMHPYKShq9gj+EYk/BXZoPvQ1d6/T+/LSCNPA==", "license": "SEE LICENSE IN README.md", "peerDependencies": { "@types/react": "*", @@ -5768,13 +6141,13 @@ } }, "node_modules/@vector-im/compound-web": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@vector-im/compound-web/-/compound-web-8.2.0.tgz", - "integrity": "sha512-we+EQ/pw2YCEl7EMPdpeqP3HZpnQcCuOHoiAYKFwF4doXBDENLpTyA8ZdX0cViT3sqvExPT0RHZ2Nlt5Y6dQNQ==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@vector-im/compound-web/-/compound-web-8.2.4.tgz", + "integrity": "sha512-Fsb/99r98ICMdtNt/bFcWtLmjyngcye9Ugqm9VDapo1VnrO0wLFJHOcm+J+SvKZwTXCaSndBklygAr5FXk0E9w==", "license": "SEE LICENSE IN README.md", "dependencies": { "@floating-ui/react": "^0.27.0", - "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-progress": "^1.1.0", @@ -5787,7 +6160,7 @@ "@fontsource/inconsolata": "^5", "@fontsource/inter": "^5", "@types/react": "*", - "@vector-im/compound-design-tokens": ">=1.6.1 <6.0.0", + "@vector-im/compound-design-tokens": ">=1.6.1 <7.0.0", "react": "^18 || ^19.0.0" }, "peerDependenciesMeta": { @@ -5797,24 +6170,24 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz", - "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", + "@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.11", + "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "react-refresh": "^0.18.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@vitest/coverage-v8": { @@ -5856,6 +6229,7 @@ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, + "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", @@ -5909,6 +6283,7 @@ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, + "license": "MIT", "dependencies": { "tinyrainbow": "^2.0.0" }, @@ -5951,6 +6326,7 @@ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^4.0.3" }, @@ -5963,6 +6339,7 @@ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, + "license": "MIT", "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", @@ -5987,13 +6364,13 @@ } }, "node_modules/@whatwg-node/fetch": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.8.tgz", - "integrity": "sha512-Rw9z3ctmeEj8QIB9MavkNJqekiu9usBCSMZa+uuAvM0lF3v70oQVCXNppMIqaV6OTZbdaHF1M2HLow58DEw+wg==", + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.10.tgz", + "integrity": "sha512-watz4i/Vv4HpoJ+GranJ7HH75Pf+OkPQ63NoVmru6Srgc8VezTArB00i/oQlnn0KWh14gM42F22Qcc9SU9mo/w==", "dev": true, "license": "MIT", "dependencies": { - "@whatwg-node/node-fetch": "^0.7.21", + "@whatwg-node/node-fetch": "^0.7.25", "urlpattern-polyfill": "^10.0.0" }, "engines": { @@ -6001,9 +6378,9 @@ } }, "node_modules/@whatwg-node/node-fetch": { - "version": "0.7.21", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.21.tgz", - "integrity": "sha512-QC16IdsEyIW7kZd77aodrMO7zAoDyyqRCTLg+qG4wqtP4JV9AA+p7/lgqMdD29XyiYdVvIdFrfI9yh7B1QvRvw==", + "version": "0.7.25", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.25.tgz", + "integrity": "sha512-szCTESNJV+Xd56zU6ShOi/JWROxE9IwCic8o5D9z5QECZloas6Ez5tUuKqXTAdu6fHFx1t6C+5gwj8smzOLjtg==", "dev": true, "license": "MIT", "dependencies": { @@ -6057,41 +6434,30 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" + "acorn": "^8.11.0" }, "engines": { - "node": ">=8" + "node": ">=0.4.0" } }, "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.0.tgz", + "integrity": "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" + "environment": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6244,13 +6610,13 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", - "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", + "@jridgewell/trace-mapping": "^0.3.30", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } @@ -6272,16 +6638,6 @@ "dev": true, "license": "MIT" }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/auto-bind": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", @@ -6333,13 +6689,6 @@ "postcss": "^8.1.0" } }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/babel-dead-code-elimination": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", @@ -6406,53 +6755,14 @@ "dev": true, "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "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==", "dev": true, "license": "Apache-2.0", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/better-opn": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", - "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "open": "^8.0.4" - }, - "engines": { - "node": ">=12.0.0" + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, "node_modules/binary-extensions": { @@ -6468,25 +6778,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -6510,159 +6801,10 @@ "node": ">=8" } }, - "node_modules/broccoli-node-api": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", - "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", - "dev": true, - "license": "MIT" - }, - "node_modules/broccoli-node-info": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", - "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "8.* || >= 10.*" - } - }, - "node_modules/broccoli-output-wrapper": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", - "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fs-extra": "^8.1.0", - "heimdalljs-logger": "^0.1.10", - "symlink-or-copy": "^1.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - } - }, - "node_modules/broccoli-output-wrapper/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/broccoli-output-wrapper/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/broccoli-output-wrapper/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/broccoli-plugin": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", - "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", - "dev": true, - "license": "MIT", - "dependencies": { - "broccoli-node-api": "^1.7.0", - "broccoli-output-wrapper": "^3.2.5", - "fs-merger": "^3.2.1", - "promise-map-series": "^0.3.0", - "quick-temp": "^0.1.8", - "rimraf": "^3.0.2", - "symlink-or-copy": "^1.3.1" - }, - "engines": { - "node": "10.* || >= 12.*" - } - }, - "node_modules/broccoli-plugin/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/broccoli-plugin/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/broccoli-plugin/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/broccoli-plugin/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", + "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", "dev": true, "funding": [ { @@ -6680,9 +6822,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", - "node-releases": "^2.0.19", + "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" }, "bin": { @@ -6721,31 +6864,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -6798,9 +6916,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001722", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz", - "integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -6831,9 +6949,9 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -6844,7 +6962,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -6905,9 +7023,9 @@ } }, "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, @@ -6921,70 +7039,6 @@ "node": ">= 16" } }, - "node_modules/cheerio": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz", - "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^10.0.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.10.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=18.17" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cheerio/node_modules/undici": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", - "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/cheerio/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -7016,67 +7070,106 @@ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", + "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", + "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" }, "engines": { - "node": ">=8" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, "license": "ISC", "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/cliui": { @@ -7112,16 +7205,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -7159,26 +7242,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -7216,25 +7279,19 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/cookie-es": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", - "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "license": "MIT" }, "node_modules/cosmiconfig": { @@ -7302,36 +7359,6 @@ "node": ">= 8" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -7387,11 +7414,17 @@ } }, "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/debug": { "version": "4.4.1", @@ -7431,39 +7464,6 @@ "node": ">=6" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/dependency-graph": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", @@ -7474,13 +7474,6 @@ "node": ">= 0.6.0" } }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true, - "license": "ISC" - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -7565,65 +7558,6 @@ "license": "MIT", "peer": true }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -7635,19 +7569,6 @@ "tslib": "^2.0.3" } }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -7666,9 +7587,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.166", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", - "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", + "version": "1.5.218", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", + "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", "dev": true, "license": "ISC" }, @@ -7679,46 +7600,38 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", "dev": true, "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/ensure-posix-path": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", - "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", - "dev": true, - "license": "ISC" - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=14" } }, - "node_modules/eol": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", - "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/error-ex": { "version": "1.3.2", @@ -7738,9 +7651,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7751,44 +7664,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -7801,16 +7702,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -7842,23 +7733,50 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -7939,11 +7857,14 @@ } }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -7978,16 +7899,29 @@ } }, "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "dev": true, "license": "MIT", "dependencies": { - "escape-string-regexp": "^1.0.5" + "is-unicode-supported": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8006,34 +7940,6 @@ "node": ">=8" } }, - "node_modules/find-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -8052,9 +7958,9 @@ } }, "node_modules/formatly": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.2.4.tgz", - "integrity": "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", "dev": true, "license": "MIT", "dependencies": { @@ -8094,101 +8000,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-merger": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", - "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "broccoli-node-api": "^1.7.0", - "broccoli-node-info": "^2.1.0", - "fs-extra": "^8.0.1", - "fs-tree-diff": "^2.0.1", - "walk-sync": "^2.2.0" - } - }, - "node_modules/fs-merger/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-merger/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/fs-merger/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/fs-mkdirp-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", - "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.8", - "streamx": "^2.12.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/fs-tree-diff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", - "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/symlink-or-copy": "^1.2.0", - "heimdalljs-logger": "^0.1.7", - "object-assign": "^4.1.0", - "path-posix": "^1.0.0", - "symlink-or-copy": "^1.1.8" - }, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8241,6 +8052,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -8250,6 +8074,23 @@ "node": ">=6" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", @@ -8297,49 +8138,6 @@ "node": ">= 6" } }, - "node_modules/glob-stream": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.3.tgz", - "integrity": "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@gulpjs/to-absolute-glob": "^4.0.0", - "anymatch": "^3.1.3", - "fastq": "^1.13.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "is-negated-glob": "^1.0.0", - "normalize-path": "^3.0.0", - "streamx": "^2.12.5" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-stream/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -8421,29 +8219,15 @@ } }, "node_modules/graphql-config/node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/graphql-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", - "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "cross-fetch": "^3.1.5" - }, - "peerDependencies": { - "graphql": "14 - 16" - } - }, "node_modules/graphql-tag": { "version": "2.12.6", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", @@ -8461,9 +8245,9 @@ } }, "node_modules/graphql-ws": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.5.tgz", - "integrity": "sha512-HzYw057ch0hx2gZjkbgk1pur4kAtgljlWRP+Gccudqm3BRrTpExjWCQ9OHdIsq47Y6lHL++1lTvuQHhgRRcevw==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.6.tgz", + "integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==", "dev": true, "license": "MIT", "engines": { @@ -8491,20 +8275,10 @@ } } }, - "node_modules/gulp-sort": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", - "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "through2": "^2.0.1" - } - }, "node_modules/happy-dom": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.1.tgz", - "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.0.4.tgz", + "integrity": "sha512-WxFtvnij6G64/MtMimnZhF0nKx3LUQKc20zjATD6tKiqOykUwQkd+2FW/DZBAFNjk4oWh0xdv/HBleGJmSY/Iw==", "dev": true, "license": "MIT", "dependencies": { @@ -8517,9 +8291,9 @@ } }, "node_modules/happy-dom/node_modules/@types/node": { - "version": "20.19.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", - "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", + "version": "20.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", + "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", "dev": true, "license": "MIT", "dependencies": { @@ -8574,51 +8348,6 @@ "dev": true, "license": "MIT" }, - "node_modules/heimdalljs": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", - "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rsvp": "~3.2.1" - } - }, - "node_modules/heimdalljs-logger": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", - "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^2.2.0", - "heimdalljs": "^0.2.6" - } - }, - "node_modules/heimdalljs-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/heimdalljs-logger/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/heimdalljs/node_modules/rsvp": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", - "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", - "dev": true, - "license": "MIT" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8635,71 +8364,20 @@ "void-elements": "3.1.0" } }, - "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", + "license": "Apache-2.0", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "node": ">=18.18.0" } }, "node_modules/i18next": { - "version": "25.4.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz", - "integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==", + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.2.tgz", + "integrity": "sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==", "funding": [ { "type": "individual", @@ -8727,138 +8405,193 @@ } } }, - "node_modules/i18next-browser-languagedetector": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", - "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, - "node_modules/i18next-http-backend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", - "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cross-fetch": "4.0.0" - } - }, - "node_modules/i18next-http-backend/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/i18next-parser": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-9.3.0.tgz", - "integrity": "sha512-VaQqk/6nLzTFx1MDiCZFtzZXKKyBV6Dv0cJMFM/hOt4/BWHWRgYafzYfVQRUzotwUwjqeNCprWnutzD/YAGczg==", + "node_modules/i18next-cli": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/i18next-cli/-/i18next-cli-1.22.1.tgz", + "integrity": "sha512-csn/GOgY0p9oLk/btdURZEmKT7RmnmSjIhmFU4NS+iL+hQxDRCkuw4umXX/zuPeKw0ge+7I0J1jE3DTTKd6Z7A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.0", - "broccoli-plugin": "^4.0.7", - "cheerio": "^1.0.0", - "colors": "^1.4.0", - "commander": "^12.1.0", - "eol": "^0.9.1", - "esbuild": "^0.25.0", - "fs-extra": "^11.2.0", - "gulp-sort": "^2.0.0", - "i18next": "^23.5.1 || ^24.2.0", - "js-yaml": "^4.1.0", - "lilconfig": "^3.1.3", - "rsvp": "^4.8.5", - "sort-keys": "^5.0.0", - "typescript": "^5.0.4", - "vinyl": "^3.0.0", - "vinyl-fs": "^4.0.0" + "@swc/core": "1.13.19", + "chalk": "5.6.2", + "chokidar": "4.0.3", + "commander": "14.0.2", + "execa": "9.6.0", + "glob": "11.0.3", + "i18next-resources-for-ts": "1.7.4", + "inquirer": "12.10.0", + "jiti": "2.6.1", + "jsonc-parser": "3.3.1", + "minimatch": "10.1.1", + "ora": "9.0.0", + "swc-walk": "1.0.0" }, "bin": { - "i18next": "bin/cli.js" + "i18next-cli": "dist/esm/cli.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || ^22.0.0", - "npm": ">=6", - "yarn": ">=1" + "node": ">=22" } }, - "node_modules/i18next-parser/node_modules/i18next": { - "version": "24.2.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", - "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", + "node_modules/i18next-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.10" + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/i18next-cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "node_modules/i18next-cli/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/i18next-cli/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/i18next-cli/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/i18next-cli/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/i18next-cli/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/i18next-cli/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/i18next-resources-for-ts": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-1.7.4.tgz", + "integrity": "sha512-3NpN2zasOWYR5zWA4JIdFhxrHxRJV8HEsbR7/GHSnotfjArjZzKvOzQnLFZ911QFmmcwq80saw8rccpHH+MYVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "yaml": "^2.7.1" + }, + "bin": { + "i18next-resources-for-ts": "bin/i18next-resources-for-ts.js" + } }, "node_modules/ignore": { "version": "5.3.2", @@ -8950,30 +8683,30 @@ "license": "ISC" }, "node_modules/inquirer": { - "version": "8.2.7", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", - "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.10.0.tgz", + "integrity": "sha512-K/epfEnDBZj2Q3NMDcgXWZye3nhSPeoJnOh8lcKWrldw54UEZfS4EmAMsAsmVbl7qKi+vjAsy39Sz4fbgRMewg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/external-editor": "^1.0.0", - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^6.0.1" + "@inquirer/ansi": "^1.0.1", + "@inquirer/core": "^10.3.0", + "@inquirer/prompts": "^7.9.0", + "@inquirer/type": "^3.0.9", + "mute-stream": "^2.0.0", + "run-async": "^4.0.5", + "rxjs": "^7.8.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/invariant": { @@ -9036,22 +8769,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9086,13 +8803,16 @@ } }, "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-lower-case": { @@ -9105,16 +8825,6 @@ "tslib": "^2.0.3" } }, - "node_modules/is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -9158,6 +8868,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -9194,16 +8917,6 @@ "tslib": "^2.0.3" } }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -9214,30 +8927,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isbot": { - "version": "5.1.28", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.28.tgz", - "integrity": "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==", + "version": "5.1.30", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.30.tgz", + "integrity": "sha512-3wVJEonAns1OETX83uWsk5IAne2S5zfDcntD2hbtU23LelSqNXzXs9zKjMPOLMzroCgIjCfjYAEHrd2D6FOkiA==", + "license": "Unlicense", "engines": { "node": ">=18" } @@ -9300,9 +8994,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9339,16 +9033,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9357,9 +9041,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -9416,10 +9100,17 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -9430,9 +9121,9 @@ } }, "node_modules/knip": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.62.0.tgz", - "integrity": "sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==", + "version": "5.66.4", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.66.4.tgz", + "integrity": "sha512-HmTnxdmoHAvwKmFktRGY1++tXRI8J36eVrOpfj/ybTVVT1QBKBlbBEN1s3cJBx9UL+hXTZDNQif+gs7fUKldbw==", "dev": true, "funding": [ { @@ -9442,27 +9133,22 @@ { "type": "opencollective", "url": "https://opencollective.com/knip" - }, - { - "type": "polar", - "url": "https://polar.sh/webpro-nl" } ], "license": "ISC", "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", - "formatly": "^0.2.4", - "jiti": "^2.4.2", + "formatly": "^0.3.0", + "jiti": "^2.6.0", "js-yaml": "^4.1.0", "minimist": "^1.2.8", - "oxc-resolver": "^11.1.0", + "oxc-resolver": "^11.12.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", - "smol-toml": "^1.3.4", + "smol-toml": "^1.4.1", "strip-json-comments": "5.0.2", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.3" + "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", @@ -9473,27 +9159,27 @@ }, "peerDependencies": { "@types/node": ">=18", - "typescript": ">=5.0.4" + "typescript": ">=5.0.4 <7" } }, "node_modules/knip/node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/lead": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", - "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "node_modules/knip/node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10.13.0" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/lilconfig": { @@ -9517,67 +9203,108 @@ "license": "MIT" }, "node_modules/listr2": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", - "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", + "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.5", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/listr2/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -9610,40 +9337,108 @@ } }, "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/loose-envify": { @@ -9660,9 +9455,9 @@ } }, "node_modules/loupe": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", - "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, @@ -9708,13 +9503,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/magicast": { @@ -9745,6 +9540,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -9755,44 +9563,6 @@ "node": ">=0.10.0" } }, - "node_modules/matcher-collection": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", - "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@types/minimatch": "^3.0.3", - "minimatch": "^3.0.2" - }, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/matcher-collection/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/matcher-collection/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/meow": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", @@ -9817,9 +9587,9 @@ } }, "node_modules/meros": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.1.tgz", - "integrity": "sha512-eV7dRObfTrckdmAz4/n7pT1njIsIJXRIZkgCiX43xEsPNy4gjXQzOYYxmGcolAMtF7HyfqRuDBh3Lgs4hmhVEw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.2.tgz", + "integrity": "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A==", "dev": true, "license": "MIT", "engines": { @@ -9861,14 +9631,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/min-indent": { @@ -9917,16 +9690,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mktemp": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", - "integrity": "sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">0.9" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9935,30 +9698,30 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.1.tgz", - "integrity": "sha512-dGSRx0AJmQVQfpGXTsAAq4JFdwdhOBdJ6sJS/jnN0ac3s0NZB6daacHF1z5Pefx+IejmvuiLWw260RlyQOf3sQ==", + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.6.tgz", + "integrity": "sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@bundled-es-modules/cookie": "^2.0.1", - "@bundled-es-modules/statuses": "^1.0.1", "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.39.1", + "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/until": "^2.1.0", - "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", + "cookie": "^1.0.2", "graphql": "^16.8.1", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", + "rettime": "^0.7.0", + "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^4.26.1", + "until-async": "^3.0.2", "yargs": "^17.7.2" }, "bin": { @@ -10006,11 +9769,14 @@ } }, "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/mz": { "version": "2.7.0", @@ -10104,9 +9870,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "dev": true, "license": "MIT" }, @@ -10130,30 +9896,34 @@ "node": ">=0.10.0" } }, - "node_modules/now-and-later": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", - "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, "license": "MIT", "dependencies": { - "once": "^1.4.0" + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" + "node": ">=18" }, "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/nullthrows": { @@ -10194,63 +9964,134 @@ } }, "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", "dev": true, "license": "MIT", "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -10259,28 +10100,34 @@ "license": "MIT" }, "node_modules/oxc-resolver": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.2.0.tgz", - "integrity": "sha512-3iJYyIdDZMDoj0ZSVBrI1gUvPBMkDC4gxonBG+7uqUyK5EslG0mCwnf6qhxK8oEU7jLHjbRBNyzflPSd3uvH7Q==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.12.0.tgz", + "integrity": "sha512-zmS2q2txiB+hS2u0aiIwmvITIJN8c8ThlWoWB762Wx5nUw8WBlttp0rzt8nnuP1cGIq9YJ7sGxfsgokm+SQk5Q==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-resolver/binding-darwin-arm64": "11.2.0", - "@oxc-resolver/binding-darwin-x64": "11.2.0", - "@oxc-resolver/binding-freebsd-x64": "11.2.0", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.2.0", - "@oxc-resolver/binding-linux-arm64-gnu": "11.2.0", - "@oxc-resolver/binding-linux-arm64-musl": "11.2.0", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.2.0", - "@oxc-resolver/binding-linux-s390x-gnu": "11.2.0", - "@oxc-resolver/binding-linux-x64-gnu": "11.2.0", - "@oxc-resolver/binding-linux-x64-musl": "11.2.0", - "@oxc-resolver/binding-wasm32-wasi": "11.2.0", - "@oxc-resolver/binding-win32-arm64-msvc": "11.2.0", - "@oxc-resolver/binding-win32-x64-msvc": "11.2.0" + "@oxc-resolver/binding-android-arm-eabi": "11.12.0", + "@oxc-resolver/binding-android-arm64": "11.12.0", + "@oxc-resolver/binding-darwin-arm64": "11.12.0", + "@oxc-resolver/binding-darwin-x64": "11.12.0", + "@oxc-resolver/binding-freebsd-x64": "11.12.0", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.12.0", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.12.0", + "@oxc-resolver/binding-linux-arm64-gnu": "11.12.0", + "@oxc-resolver/binding-linux-arm64-musl": "11.12.0", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.12.0", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.12.0", + "@oxc-resolver/binding-linux-riscv64-musl": "11.12.0", + "@oxc-resolver/binding-linux-s390x-gnu": "11.12.0", + "@oxc-resolver/binding-linux-x64-gnu": "11.12.0", + "@oxc-resolver/binding-linux-x64-musl": "11.12.0", + "@oxc-resolver/binding-wasm32-wasi": "11.12.0", + "@oxc-resolver/binding-win32-arm64-msvc": "11.12.0", + "@oxc-resolver/binding-win32-ia32-msvc": "11.12.0", + "@oxc-resolver/binding-win32-x64-msvc": "11.12.0" } }, "node_modules/p-limit": { @@ -10299,67 +10146,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -10435,57 +10221,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "dev": true, "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=18" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pascal-case": { @@ -10510,16 +10256,6 @@ "tslib": "^2.0.3" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -10547,13 +10283,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-posix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", - "dev": true, - "license": "ISC" - }, "node_modules/path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", @@ -10626,9 +10355,9 @@ "license": "MIT" }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -10643,9 +10372,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -10868,9 +10597,9 @@ "license": "MIT" }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -10913,12 +10642,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/promise": { "version": "7.3.1", @@ -10930,16 +10668,6 @@ "asap": "~2.0.3" } }, - "node_modules/promise-map-series": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", - "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "10.* || >= 12.*" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10961,99 +10689,27 @@ ], "license": "MIT" }, - "node_modules/quick-temp": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", - "integrity": "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mktemp": "~0.4.0", - "rimraf": "^2.5.4", - "underscore.string": "~3.3.4" - } - }, - "node_modules/quick-temp/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/quick-temp/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/quick-temp/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/quick-temp/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-docgen": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.0.tgz", - "integrity": "sha512-kmob/FOTwep7DUWf9KjuenKX0vyvChr3oTdvvPt09V60Iz75FJp+T/0ZeHMbAfJj2WaVWqAPP5Hmm3PYzSPPKg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.1.tgz", + "integrity": "sha512-kQKsqPLplY3Hx4jGnM3jpQcG3FQDt7ySz32uTHt3C9HAe45kNXG+3o16Eqn3Fw1GtMfHoN3b4J/z2e6cZJCmqQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.18.9", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9", - "@types/babel__core": "^7.18.0", - "@types/babel__traverse": "^7.18.0", + "@babel/core": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.20.7", "@types/doctrine": "^0.0.9", "@types/resolve": "^1.20.2", "doctrine": "^3.0.0", @@ -11069,33 +10725,35 @@ "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", "dev": true, + "license": "MIT", "peerDependencies": { "typescript": ">= 4.3.x" } }, "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.1.1" + "react": "^19.2.0" } }, "node_modules/react-i18next": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.2.tgz", - "integrity": "sha512-xJxq7ibnhUlMvd82lNC4te1GxGUMoM1A05KKyqoqsBXVZtEvZg/fz/fnVzdlY/hhQ3SpP/79qCocZOtICGhd3g==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.0.tgz", + "integrity": "sha512-XGYIVU6gCOL4UQsfp87WbbvBc2WvgdkEDI8r4TwACzFg1bXY8pd1d9Cw6u9WJ2soTKHKaF1xQEyWA3/dUvtAGw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.27.6", - "html-parse-stringify": "^3.0.1" + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "i18next": ">= 25.4.1", + "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, @@ -11120,9 +10778,9 @@ "peer": true }, "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -11208,21 +10866,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11339,16 +10982,6 @@ "dev": true, "license": "MIT" }, - "node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11397,19 +11030,6 @@ "node": ">=8" } }, - "node_modules/resolve-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", - "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "value-or-function": "^4.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -11421,25 +11041,28 @@ } }, "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "node_modules/rettime": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", + "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", "dev": true, - "license": "ISC" + "license": "MIT" }, "node_modules/reusify": { "version": "1.1.0", @@ -11479,39 +11102,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/balanced-match": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", - "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz", - "integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^3.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/rimraf/node_modules/glob": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", - "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -11543,9 +11143,9 @@ } }, "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", "dev": true, "license": "ISC", "engines": { @@ -11553,13 +11153,13 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.2.tgz", - "integrity": "sha512-+9TJCIYXgZ2Dm5LxVCFsa8jOm+evMwXHFI0JM1XROmkfkpz8/iLLDh+TwSmyIBrs6C6Xu9294/fq8cBA+P6AqA==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^4.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -11586,13 +11186,13 @@ } }, "node_modules/rollup": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", - "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -11602,50 +11202,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.43.0", - "@rollup/rollup-android-arm64": "4.43.0", - "@rollup/rollup-darwin-arm64": "4.43.0", - "@rollup/rollup-darwin-x64": "4.43.0", - "@rollup/rollup-freebsd-arm64": "4.43.0", - "@rollup/rollup-freebsd-x64": "4.43.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", - "@rollup/rollup-linux-arm-musleabihf": "4.43.0", - "@rollup/rollup-linux-arm64-gnu": "4.43.0", - "@rollup/rollup-linux-arm64-musl": "4.43.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-musl": "4.43.0", - "@rollup/rollup-linux-s390x-gnu": "4.43.0", - "@rollup/rollup-linux-x64-gnu": "4.43.0", - "@rollup/rollup-linux-x64-musl": "4.43.0", - "@rollup/rollup-win32-arm64-msvc": "4.43.0", - "@rollup/rollup-win32-ia32-msvc": "4.43.0", - "@rollup/rollup-win32-x64-msvc": "4.43.0", + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" } }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "6.* || >= 7.*" - } - }, "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", "dev": true, "license": "MIT", "engines": { @@ -11686,27 +11270,6 @@ "tslib": "^2.1.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11715,29 +11278,19 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/scuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", - "integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==", - "dev": true, + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/sentence-case": { @@ -11756,14 +11309,16 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", - "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", + "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -11859,24 +11414,55 @@ } }, "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/smol-toml": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.4.tgz", - "integrity": "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz", + "integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -11909,22 +11495,6 @@ "seroval-plugins": "~1.3.0" } }, - "node_modules/sort-keys": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz", - "integrity": "sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-obj": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -11955,13 +11525,6 @@ "tslib": "^2.0.3" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -11986,28 +11549,40 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/storybook": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.3.tgz", - "integrity": "sha512-Sm+qP3iGb/QKx/jTYdfE0mIeTmA2HF+5k9fD70S9oOJq3F9UdW8MLgs+5PE+E/xAfDjZU4OWAKEOyA6EYIvQHg==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.0.8.tgz", + "integrity": "sha512-vQMufKKA9TxgoEDHJv3esrqUkjszuuRiDkThiHxENFPdQawHhm2Dei+iwNRwH5W671zTDy9iRT9P1KDjcU5Iyw==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", + "@storybook/icons": "^1.6.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", - "better-opn": "^3.0.2", "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-register": "^3.5.0", "recast": "^0.23.5", "semver": "^7.6.2", "ws": "^8.18.0" }, "bin": { - "storybook": "bin/index.cjs" + "storybook": "dist/bin/dispatcher.js" }, "funding": { "type": "opencollective", @@ -12022,59 +11597,17 @@ } } }, - "node_modules/storybook-i18n": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/storybook-i18n/-/storybook-i18n-4.0.5.tgz", - "integrity": "sha512-uy6k7N5VU8PRSoMo6tVYo1WNSDRd8Z3goSku7J1Cz8A8WseBN5xAnGZ/IbO5DLUOVBetLZdaKHBVoLKbYidHjQ==", + "node_modules/storybook/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/icons": "^1.4.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "peerDependencies": { - "storybook": "^9.0.0" - } - }, - "node_modules/storybook-react-i18next": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/storybook-react-i18next/-/storybook-react-i18next-4.0.11.tgz", - "integrity": "sha512-p6gcz8//n7mtBaP75yZx910/t9Z4aIwOP+xzCvxwTzWL19NT1YGTR4GyR0ybzbEebqlPJtJVHnpGKQQD4wRyYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "storybook-i18n": "^4.0.5" - }, - "peerDependencies": { - "i18next": "^22.0.0 || ^23.0.0 || ^24.0.0 || ^25.0.0", - "i18next-browser-languagedetector": "^7.0.0 || ^8.0.0", - "i18next-http-backend": "^2.0.0 || ^3.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-i18next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", - "storybook": "^9.0.0" - } - }, - "node_modules/stream-composer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", - "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.13.2" - } - }, - "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" + "engines": { + "node": ">=10" } }, "node_modules/strict-event-emitter": { @@ -12084,16 +11617,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-env-interpolation": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", @@ -12169,15 +11692,25 @@ "node": ">=4" } }, - "node_modules/strip-indent": { + "node_modules/strip-final-newline": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, "license": "MIT", - "dependencies": { - "min-indent": "^1.0.1" + "engines": { + "node": ">=18" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.0.tgz", + "integrity": "sha512-OA95x+JPmL7kc7zCu+e+TeYxEiaIyndRx0OrBcK2QPPH09oAndr2ALvymxWA+Lx1PYYvFUm4O63pRkdJAaW96w==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -12278,9 +11811,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.27.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz", - "integrity": "sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA==", + "version": "5.29.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.5.tgz", + "integrity": "sha512-2zFnjONgLXlz8gLToRKvXHKJdqXF6UGgCmv65i8T6i/UrjDNyV1fIQ7FauZA40SaivlGKEvW2tw9XDyDhfcXqQ==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -12296,12 +11829,18 @@ "tslib": "^2.0.3" } }, - "node_modules/symlink-or-copy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", - "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "node_modules/swc-walk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/swc-walk/-/swc-walk-1.0.0.tgz", + "integrity": "sha512-QnEvBZ/ZRsUrXCz/Z3Kto06xUsoqUTo3doj/UvOD0RfamEgqlhpgpyCykFAwiUcuDrODShzlxuDqDPf2Wc+DvQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "acorn-walk": "^8.3.4" + }, + "engines": { + "node": ">=20.2.0" + } }, "node_modules/sync-fetch": { "version": "0.6.0-2", @@ -12354,9 +11893,9 @@ "license": "MIT" }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12368,7 +11907,7 @@ "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.6", + "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", @@ -12377,7 +11916,7 @@ "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", @@ -12436,16 +11975,6 @@ "node": ">=4" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -12461,16 +11990,6 @@ "node": ">=18" } }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -12494,57 +12013,6 @@ "node": ">=0.8" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/timeout-signal": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/timeout-signal/-/timeout-signal-2.0.0.tgz", @@ -12582,14 +12050,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -12623,6 +12091,7 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -12638,22 +12107,22 @@ } }, "node_modules/tldts": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.12.tgz", - "integrity": "sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz", + "integrity": "sha512-lMNHE4aSI3LlkMUMicTmAG3tkkitjOQGDTFboPJwAg2kJXKP1ryWEyqujktg5qhrFZOkk5YFzgkxg3jErE+i5w==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.12" + "tldts-core": "^7.0.14" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.12.tgz", - "integrity": "sha512-3K76aXywJFduGRsOYoY5JzINLs/WMlOkeDwPL+8OCPq2Rh39gkSDtWAxdJQlWjpun/xF/LHf29yqCi6VC/rHDA==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.14.tgz", + "integrity": "sha512-viZGNK6+NdluOJWwTO9olaugx0bkKhscIdriQQ+lNNhwitIKvb+SvhbYgnCz6j9p7dX3cJntt4agQAKMXLjJ5g==", "dev": true, "license": "MIT" }, @@ -12670,19 +12139,6 @@ "node": ">=8.0" } }, - "node_modules/to-through": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", - "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -12749,9 +12205,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.2.tgz", - "integrity": "sha512-He0ZWr41gLa4vD30Au3yuwpe0HXaCZbclvl8RBieUiJ9aFnPMWUPIyvw3RU8+1Crjfcrauvitae2a4tUzRAGsw==", + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", "dev": true, "license": "MIT", "dependencies": { @@ -12768,33 +12224,10 @@ "fsevents": "~2.3.3" } }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -12806,9 +12239,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", "dev": true, "funding": [ { @@ -12842,54 +12275,17 @@ "node": ">=0.10.0" } }, - "node_modules/underscore.string": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", - "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "^1.1.1", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, - "node_modules/undici/node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { @@ -12899,13 +12295,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "dev": true, - "license": "ISC" - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -12943,17 +12332,29 @@ } }, "node_modules/unplugin": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", - "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", + "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.14.0", + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.12.0" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" } }, "node_modules/update-browserslist-db": { @@ -13058,9 +12459,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -13087,16 +12488,6 @@ } } }, - "node_modules/value-or-function": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", - "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -13110,136 +12501,25 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, - "node_modules/vinyl": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", - "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.2", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-contents": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", - "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^5.0.0", - "vinyl": "^3.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-contents/node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/vinyl-contents/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/vinyl-fs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", - "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fs-mkdirp-stream": "^2.0.1", - "glob-stream": "^8.0.3", - "graceful-fs": "^4.2.11", - "iconv-lite": "^0.6.3", - "is-valid-glob": "^1.0.0", - "lead": "^4.0.0", - "normalize-path": "3.0.0", - "resolve-options": "^2.0.0", - "stream-composer": "^1.0.2", - "streamx": "^2.14.0", - "to-through": "^3.0.0", - "value-or-function": "^4.0.0", - "vinyl": "^3.0.1", - "vinyl-sourcemap": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-sourcemap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", - "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "convert-source-map": "^2.0.0", - "graceful-fs": "^4.2.10", - "now-and-later": "^3.0.0", - "streamx": "^2.12.5", - "vinyl": "^3.0.0", - "vinyl-contents": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -13248,14 +12528,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -13350,15 +12630,15 @@ } }, "node_modules/vite-plugin-graphql-codegen": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/vite-plugin-graphql-codegen/-/vite-plugin-graphql-codegen-3.6.1.tgz", - "integrity": "sha512-6uTRv8jD1pp9kt6StjOL6BGj166qVXmRwe06m9I1qtxjIVf+i7aF95gFv0NKxhEXXaDr1hFVlpp+3Ts+SQAy4g==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/vite-plugin-graphql-codegen/-/vite-plugin-graphql-codegen-3.7.0.tgz", + "integrity": "sha512-6TXkpUPZunV+RHP+A5R6ohar6WWjfWxTN8OpBsrZmGlJlVEpwc+2FaquAtUwO1B6kzxEomqJ7q5Idnns57hTxg==", "dev": true, "license": "MIT", "peerDependencies": { - "@graphql-codegen/cli": ">=1.0.0 <6.0.0", + "@graphql-codegen/cli": ">=1.0.0 <7.0.0", "graphql": ">=14.0.0 <17.0.0", - "vite": ">=2.7.0 <7.0.0" + "vite": ">=2.7.0 <8.0.0" } }, "node_modules/vite-plugin-manifest-sri": { @@ -13450,46 +12730,6 @@ "node": ">=0.10.0" } }, - "node_modules/walk-sync": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", - "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "^3.0.3", - "ensure-posix-path": "^1.1.0", - "matcher-collection": "^2.0.0", - "minimatch": "^3.0.4" - }, - "engines": { - "node": "8.* || >= 10.*" - } - }, - "node_modules/walk-sync/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/walk-sync/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", @@ -13500,16 +12740,6 @@ "node": "20 || >=22" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -13534,19 +12764,6 @@ "dev": true, "license": "MIT" }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -13650,9 +12867,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { @@ -13671,16 +12888,6 @@ } } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -13699,9 +12906,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { @@ -13711,13 +12918,6 @@ "node": ">= 14.6" } }, - "node_modules/yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -13760,10 +12960,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { + "node_modules/yoctocolors": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "dev": true, "license": "MIT", "engines": { @@ -13774,27 +12987,14 @@ } }, "node_modules/zod": { - "version": "3.25.63", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.63.tgz", - "integrity": "sha512-3ttCkqhtpncYXfP0f6dsyabbYV/nEUW+Xlu89jiXbTBifUfjaSqXOG6JnQPLtqt87n7KAmnMqcjay6c0Wq0Vbw==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-validation-error": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.0.tgz", - "integrity": "sha512-IWK6O51sRkq0YsnYD2oLDuK2BNsIjYUlR0+1YSd4JyBzm6/892IWroUnLc7oW4FU+b0f6948BHi6H8MDcqpOGw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 6a5d40c66..272a7d45b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "generate": "graphql-codegen && i18next", - "lint": "graphql-codegen && biome check && tsc && i18next --fail-on-warnings --fail-on-update", + "generate": "graphql-codegen && i18next-cli extract", + "lint": "graphql-codegen && biome check && tsc && i18next-cli extract --ci", "format": "biome format --write", "build": "rimraf ./dist/ && vite build", "preview": "vite preview", @@ -14,74 +14,72 @@ "coverage": "vitest run --coverage", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "i18n": "i18next", + "i18n": "i18next-cli", "knip": "knip" }, "dependencies": { - "@fontsource/inconsolata": "^5.2.6", - "@fontsource/inter": "^5.2.6", - "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.14", - "@tanstack/react-query": "^5.85.5", - "@tanstack/react-router": "^1.131.27", - "@vector-im/compound-design-tokens": "5.0.2", - "@vector-im/compound-web": "^8.2.0", + "@fontsource/inconsolata": "^5.2.8", + "@fontsource/inter": "^5.2.8", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@tanstack/react-query": "^5.90.7", + "@tanstack/react-router": "^1.131.44", + "@vector-im/compound-design-tokens": "6.0.0", + "@vector-im/compound-web": "^8.2.4", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "classnames": "^2.5.1", "date-fns": "^4.1.0", - "i18next": "^25.4.2", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-i18next": "^15.7.2", - "swagger-ui-dist": "^5.27.1", + "i18next": "^25.6.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.3.0", + "swagger-ui-dist": "^5.29.5", "valibot": "^1.1.0", "vaul": "^1.1.2" }, "devDependencies": { - "@biomejs/biome": "^2.1.2", + "@biomejs/biome": "^2.3.2", "@browser-logos/chrome": "^2.0.0", "@browser-logos/firefox": "^3.0.10", "@browser-logos/safari": "^2.1.0", - "@codecov/vite-plugin": "^1.9.1", - "@graphql-codegen/cli": "^5.0.7", - "@graphql-codegen/client-preset": "^4.8.3", + "@graphql-codegen/cli": "^6.0.1", + "@graphql-codegen/client-preset": "^5.1.1", "@graphql-codegen/typescript-msw": "^3.0.1", - "@storybook/addon-docs": "^9.1.3", - "@storybook/react-vite": "^9.1.3", - "@tanstack/react-query-devtools": "^5.85.5", - "@tanstack/react-router-devtools": "^1.131.27", - "@tanstack/router-plugin": "^1.131.27", - "@testing-library/jest-dom": "^6.8.0", + "@storybook/addon-docs": "^10.0.8", + "@storybook/react-vite": "^10.0.8", + "@tanstack/react-query-devtools": "^5.90.2", + "@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.3.0", - "@types/react": "19.1.10", - "@types/react-dom": "19.1.7", + "@types/node": "^24.9.1", + "@types/react": "19.2.2", + "@types/react-dom": "19.2.2", "@types/swagger-ui-dist": "^3.30.6", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.21", "browserslist-to-esbuild": "^2.1.1", "graphql": "^16.11.0", - "happy-dom": "^18.0.1", - "i18next-parser": "^9.3.0", - "knip": "^5.62.0", - "msw": "^2.11.1", + "happy-dom": "^20.0.4", + "i18next-cli": "^1.22.1", + "knip": "^5.66.4", + "msw": "^2.11.6", "msw-storybook-addon": "^2.0.5", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "postcss-nesting": "^13.0.2", "rimraf": "^6.0.1", - "storybook": "^9.0.1", - "storybook-react-i18next": "4.0.11", - "tailwindcss": "^3.4.17", - "typescript": "^5.9.2", - "vite": "6.3.5", + "storybook": "^10.0.5", + "tailwindcss": "^3.4.18", + "typescript": "^5.9.3", + "vite": "7.2.2", "vite-plugin-compression": "^0.5.1", - "vite-plugin-graphql-codegen": "^3.6.1", + "vite-plugin-graphql-codegen": "^3.7.0", "vite-plugin-manifest-sri": "^0.2.0", - "vitest": "^3.2.3" + "vitest": "^3.2.4" }, "msw": { "workerDirectory": [ diff --git a/frontend/src/components/Separator/Separator.tsx b/frontend/src/components/Separator/Separator.tsx index 886c63040..a0472231e 100644 --- a/frontend/src/components/Separator/Separator.tsx +++ b/frontend/src/components/Separator/Separator.tsx @@ -5,6 +5,7 @@ // biome-ignore-all lint/a11y/useFocusableInteractive: this is a false positive // biome-ignore-all lint/a11y/useAriaPropsForRole: this is a false positive +// biome-ignore-all lint/a11y/useSemanticElements: I don't want to use an
import cx from "classnames"; import { forwardRef } from "react"; diff --git a/frontend/src/components/SessionDetail/SessionInfo.tsx b/frontend/src/components/SessionDetail/SessionInfo.tsx index e170e9487..c5856587a 100644 --- a/frontend/src/components/SessionDetail/SessionInfo.tsx +++ b/frontend/src/components/SessionDetail/SessionInfo.tsx @@ -3,10 +3,11 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +import IconAdmin from "@vector-im/compound-design-tokens/assets/web/icons/admin"; import IconChat from "@vector-im/compound-design-tokens/assets/web/icons/chat"; import IconComputer from "@vector-im/compound-design-tokens/assets/web/icons/computer"; -import IconErrorSolid from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info"; +import IconRoom from "@vector-im/compound-design-tokens/assets/web/icons/room"; import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send"; import IconUserProfile from "@vector-im/compound-design-tokens/assets/web/icons/user-profile"; import { @@ -68,7 +69,7 @@ export const ScopeSendMessages: React.FC = () => { const ScopeSynapseAdmin: React.FC = () => { const { t } = useTranslation(); return ( - + {t("mas.scope.synapse_admin")} ); @@ -77,7 +78,7 @@ const ScopeSynapseAdmin: React.FC = () => { const ScopeMasAdmin: React.FC = () => { const { t } = useTranslation(); return ( - + {t("mas.scope.mas_admin")} ); diff --git a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap index d07848769..7849c7661 100644 --- a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap +++ b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap @@ -29,10 +29,10 @@ exports[` > renders a compatability session details 1`] = ` > element.io: Unknown device