diff --git a/.env.example b/.env.example index 99b2c1cb..770a7e97 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,10 @@ # The base URL where the app is hosted. TINYAUTH_APPURL= +# Path to config file. +TINYAUTH_CONFIGFILE= +# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment. +TINYAUTH_LABELPROVIDER="auto" # database config @@ -97,8 +101,6 @@ TINYAUTH_AUTH_SESSIONMAXLIFETIME=0 TINYAUTH_AUTH_LOGINTIMEOUT=300 # Maximum login retries. TINYAUTH_AUTH_LOGINMAXRETRIES=3 -# Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically. -TINYAUTH_AUTH_LOCKDOWNENABLED=true # Comma-separated list of trusted proxy addresses. TINYAUTH_AUTH_TRUSTEDPROXIES= # ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow. @@ -130,9 +132,9 @@ TINYAUTH_APPS_name_RESPONSE_BASICAUTH_USERNAME= TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORD= # Path to the file containing the basic auth password. TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORDFILE= -# Comma-separated list of allowed paths. +# Disable authentication for only paths that match the regex string. TINYAUTH_APPS_name_PATH_ALLOW= -# Comma-separated list of blocked paths. +# Enable authentication for only paths that match the regex string. TINYAUTH_APPS_name_PATH_BLOCK= # Comma-separated list of required LDAP groups. TINYAUTH_APPS_name_LDAP_GROUPS= @@ -221,6 +223,11 @@ TINYAUTH_LDAP_AUTHKEY= # Cache duration for LDAP group membership in seconds. TINYAUTH_LDAP_GROUPCACHETTL=900 +# experimental config + +# Enable the OAuth bridge, uses a new way to format OAuth user information. +TINYAUTH_EXPERIMENTAL_OAUTHBRIDGEENABLED=false + # tailscale config # Enable Tailscale integration. @@ -233,8 +240,6 @@ TINYAUTH_TAILSCALE_APITOKENFILE= TINYAUTH_TAILSCALE_TAILNET= # Cache duration for Tailscale device and user lists in seconds. TINYAUTH_TAILSCALE_CACHEDURATION=300 -# Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment. -TINYAUTH_LABELPROVIDER="auto" # log config diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ddf5cc54..f3c4c16a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,4 +28,4 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb9dcfb0..51319abe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,15 +13,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: package_json_file: ./frontend/package.json - name: Setup go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "^1.26.4" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9c569bf2..09ce71b9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Delete old release run: gh release delete --cleanup-tag --yes nightly || echo release not found @@ -23,7 +23,7 @@ jobs: REPO: ${{ github.event.repository.name }} - name: Create release - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3 with: prerelease: true tag_name: nightly @@ -38,7 +38,7 @@ jobs: BUILD_TAGS: ${{ steps.metadata.outputs.BUILD_TAGS }} steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: nightly @@ -58,17 +58,17 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: nightly - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: package_json_file: ./frontend/package.json - name: Install go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "^1.26.4" @@ -107,17 +107,17 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: nightly - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: package_json_file: ./frontend/package.json - name: Install go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "^1.26.4" @@ -156,7 +156,7 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: nightly @@ -167,7 +167,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -215,7 +215,7 @@ jobs: - image-build steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: nightly @@ -226,7 +226,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -274,7 +274,7 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: nightly @@ -285,7 +285,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -333,7 +333,7 @@ jobs: - image-build-arm steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: nightly @@ -344,7 +344,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -399,7 +399,7 @@ jobs: merge-multiple: true - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -438,7 +438,7 @@ jobs: merge-multiple: true - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -476,7 +476,7 @@ jobs: merge-multiple: true - name: Release - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3 with: files: binaries/* tag_name: nightly diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dfe2efdc..8515a612 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: BUILD_TAGS: ${{ steps.metadata.outputs.BUILD_TAGS }} steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate metadata id: metadata @@ -38,15 +38,15 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: package_json_file: ./frontend/package.json - name: Install go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "^1.26.4" @@ -84,15 +84,15 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: package_json_file: ./frontend/package.json - name: Install go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "^1.26.4" @@ -130,7 +130,7 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Docker meta id: meta @@ -139,7 +139,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -187,7 +187,7 @@ jobs: - image-build steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Docker meta id: meta @@ -196,7 +196,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -244,7 +244,7 @@ jobs: - generate-metadata steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Docker meta id: meta @@ -253,7 +253,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -301,7 +301,7 @@ jobs: - image-build-arm steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Docker meta id: meta @@ -310,7 +310,7 @@ jobs: images: ghcr.io/${{ github.repository_owner }}/tinyauth - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -366,7 +366,7 @@ jobs: merge-multiple: true - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -407,7 +407,7 @@ jobs: merge-multiple: true - name: Login to GitHub Container Registry - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -449,6 +449,6 @@ jobs: merge-multiple: true - name: Release - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3 with: files: binaries/* diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 892a2c33..c8bc3e7f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - name: Run analysis - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc with: results_file: results.sarif results_format: sarif @@ -38,6 +38,6 @@ jobs: retention-days: 5 - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: results.sarif diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index eb1429b7..c3f30974 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate Sponsors - uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1 + uses: JamesIves/github-sponsors-readme-action@02650b8cd445fc16dfef73195f9c406dce041623 # v1.6.1 with: token: ${{ secrets.SPONSORS_GENERATOR_PAT }} active-only: false diff --git a/Dockerfile b/Dockerfile index 548a3b24..1b8c1195 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Site builder -FROM node:26.4-alpine3.23 AS frontend-builder +FROM node:26.7-alpine3.23 AS frontend-builder WORKDIR /frontend @@ -39,6 +39,7 @@ RUN go mod download COPY ./cmd ./cmd COPY ./internal ./internal +COPY ./pkg ./pkg COPY --from=frontend-builder /frontend/dist ./internal/assets/dist RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \ diff --git a/Dockerfile.dev b/Dockerfile.dev index 1ea1c5f0..d4a9c386 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -12,6 +12,7 @@ RUN go install github.com/go-delve/delve/cmd/dlv@v1.26.3 COPY ./cmd ./cmd COPY ./internal ./internal +COPY ./pkg ./pkg COPY ./air.toml ./ EXPOSE 3000 diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 65f22ac3..8d926dec 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -1,5 +1,5 @@ # Site builder -FROM node:26.4-alpine3.23 AS frontend-builder +FROM node:26.7-alpine3.23 AS frontend-builder WORKDIR /frontend @@ -39,6 +39,7 @@ RUN go mod download COPY ./cmd ./cmd/ COPY ./internal ./internal +COPY ./pkg ./pkg COPY --from=frontend-builder /frontend/dist ./internal/assets/dist RUN CGO_ENABLED=0 go build -tags "${BUILD_TAGS}" -ldflags "${LDFLAGS} \ diff --git a/Makefile b/Makefile index 40ce178f..42d0bce9 100644 --- a/Makefile +++ b/Makefile @@ -78,18 +78,10 @@ test-race: dev: docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build -# Development - Infisical -dev-infisical: - infisical run --env=dev -- docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build - # Production prod: docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans -# Production - Infisical -prod-infisical: - infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans - # SQL sql: sqlc generate diff --git a/README.md b/README.md index 76927781..4d5aeb31 100644 --- a/README.md +++ b/README.md @@ -62,28 +62,30 @@ If you like, you can help translate Tinyauth into more languages by visiting the Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file. - -## Hosting Partners - -If you use one of our partners, you can help support us while getting a great hosting deal. - -
- -
- ## Sponsors -A big thank you to the following people for providing me with more coffee: +A big thank-you to the following individuals and companies +for their support for Tinyauth. + +### Individuals + +A huge thank you to [selfh.st](https://selfh.st) for their generous donation to Tinyauth. + +selfh.st + +--- User avatar: erwinkramer  User avatar: nicotsx  User avatar: SimpleHomelab  User avatar: jmadden91  User avatar: tribor  User avatar: eliasbenb  User avatar: afunworm  User avatar: chip-well  User avatar: Lancelot-Enguerrand  User avatar: allgoewer  User avatar: NEANC  User avatar: axjab  User avatar: stegratech  User avatar: apearson  User avatar: Micky5991   +### Companies and Organizations + +- [JetBrains for Open-Source](https://jb.gg/OpenSource) +- [CodeRabbit AI](https://www.coderabbit.ai) +- [Dosu](https://dosu.dev/) +- [InstaPods](https://instapods.com) - [Deploy Tinyauth from 3$/month](https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth) + ## Acknowledgements - **Freepik** for providing the police hat and badge. - **Renee French** for the original gopher logo. -- **Coderabbit AI** for providing free AI code reviews. - **Syrhu** for providing the background image of the app. - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=tinyauthapp/tinyauth&type=Date)](https://www.star-history.com/#tinyauthapp/tinyauth&Date) diff --git a/assets/screenshot.png b/assets/screenshot.png index 62a96bf8..9470b067 100644 Binary files a/assets/screenshot.png and b/assets/screenshot.png differ diff --git a/cmd/tinyauth/generate_totp.go b/cmd/tinyauth/generate_totp.go index 696af69a..1cc7df80 100644 --- a/cmd/tinyauth/generate_totp.go +++ b/cmd/tinyauth/generate_totp.go @@ -116,11 +116,11 @@ func generateTotpCmd() *cli.Command { userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret) fmt.Print("\nOr add the following TOTP secret to your authenticator app: ") - fmt.Print(colors.green.Render(secret)) + fmt.Print(colors.blue.Render(secret)) fmt.Print("\n\n") fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username) - fmt.Print(colors.green.Render(userStr)) + fmt.Print(colors.blue.Render(userStr)) fmt.Print("\n") return nil diff --git a/cmd/tinyauth/tinyauth.go b/cmd/tinyauth/tinyauth.go index a39e49c2..506cb0ca 100644 --- a/cmd/tinyauth/tinyauth.go +++ b/cmd/tinyauth/tinyauth.go @@ -32,6 +32,7 @@ func main() { Configuration: tConfig, Resources: loaders, Run: func(_ []string) error { + // enable this on experimental features if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) { colors := getColors() fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.") diff --git a/crowdin.yml b/crowdin.yml index 902d3d12..aa26502a 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,12 +1,6 @@ -"base_path": "." -"base_url": "https://api.crowdin.com" - -"preserve_hierarchy": true - +base_path: . +base_url: 'https://api.crowdin.com' +preserve_hierarchy: 1 files: - [ - { - "source": "/frontend/src/lib/i18n/locales/en.json", - "translation": "/frontend/src/lib/i18n/locales/%locale%.json", - }, - ] + - source: /frontend/src/lib/i18n/locales/en.json + translation: /frontend/src/lib/i18n/locales/%locale%.json diff --git a/frontend/package.json b/frontend/package.json index 54f010a0..910237c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,49 +12,49 @@ }, "packageManager": "pnpm@11.1.2", "dependencies": { - "@hookform/resolvers": "^5.4.0", - "@radix-ui/react-dropdown-menu": "^2.1.19", - "@radix-ui/react-label": "^2.1.11", - "@radix-ui/react-select": "^2.3.2", - "@radix-ui/react-separator": "^1.1.11", - "@radix-ui/react-slot": "^1.3.0", - "@tailwindcss/vite": "^4.3.2", - "@tanstack/react-query": "^5.101.2", - "axios": "^1.18.1", + "@hookform/resolvers": "^5.7.1", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slot": "^1.3.3", + "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-query": "^5.101.4", + "axios": "^1.19.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^26.3.4", + "i18next": "^26.3.6", "i18next-browser-languagedetector": "^8.2.1", - "i18next-resources-to-backend": "^1.2.1", - "lucide-react": "^1.23.0", + "i18next-resources-to-backend": "^1.2.3", + "lucide-react": "^1.31.0", "next-themes": "^0.4.6", - "radix-ui": "^1.6.1", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-hook-form": "^7.81.0", - "react-i18next": "^17.0.8", + "radix-ui": "^1.6.7", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-hook-form": "^7.85.0", + "react-i18next": "^17.0.11", "react-markdown": "^10.1.0", - "react-router": "^8.1.0", - "sonner": "^2.0.7", + "react-router": "^8.3.0", + "sonner": "^2.0.8", "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.3.2", + "tailwindcss": "^4.3.3", "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@tanstack/eslint-plugin-query": "^5.101.2", - "@types/node": "^26.1.0", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", - "eslint": "^10.6.0", + "@tanstack/eslint-plugin-query": "^5.101.4", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", + "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.7.0", + "globals": "^17.9.0", "rollup-plugin-visualizer": "^7.0.1", "tw-animate-css": "^1.4.0", "typescript": "~6.0.2", - "typescript-eslint": "^8.62.1", - "vite": "^8.1.3" + "typescript-eslint": "^8.66.0", + "vite": "^8.2.1" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 4f6eb23c..1d220dce 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -9,32 +9,32 @@ importers: .: dependencies: '@hookform/resolvers': - specifier: ^5.4.0 - version: 5.4.0(react-hook-form@7.81.0(react@19.2.7)) + specifier: ^5.7.1 + version: 5.7.1(react-hook-form@7.85.0(react@19.2.8))(zod@4.4.3) '@radix-ui/react-dropdown-menu': - specifier: ^2.1.19 - version: 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^2.1.24 + version: 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-label': - specifier: ^2.1.11 - version: 2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-select': - specifier: ^2.3.2 - version: 2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^2.3.7 + version: 2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-separator': - specifier: ^1.1.11 - version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-slot': - specifier: ^1.3.0 - version: 1.3.0(@types/react@19.2.17)(react@19.2.7) + specifier: ^1.3.3 + version: 1.3.3(@types/react@19.2.18)(react@19.2.8) '@tailwindcss/vite': - specifier: ^4.3.2 - version: 4.3.2(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)) + specifier: ^4.3.3 + version: 4.3.3(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0)) '@tanstack/react-query': - specifier: ^5.101.2 - version: 5.101.2(react@19.2.7) + specifier: ^5.101.4 + version: 5.101.4(react@19.2.8) axios: - specifier: ^1.18.1 - version: 1.18.1 + specifier: ^1.19.0 + version: 1.19.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -42,87 +42,87 @@ importers: specifier: ^2.1.1 version: 2.1.1 i18next: - specifier: ^26.3.4 - version: 26.3.4(typescript@6.0.3) + specifier: ^26.3.6 + version: 26.3.6(typescript@6.0.3) i18next-browser-languagedetector: specifier: ^8.2.1 version: 8.2.1 i18next-resources-to-backend: - specifier: ^1.2.1 - version: 1.2.1 + specifier: ^1.2.3 + version: 1.2.3 lucide-react: - specifier: ^1.23.0 - version: 1.23.0(react@19.2.7) + specifier: ^1.31.0 + version: 1.31.0(react@19.2.8) next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) radix-ui: - specifier: ^1.6.1 - version: 1.6.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^1.6.7 + version: 1.6.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: - specifier: ^19.2.7 - version: 19.2.7 + specifier: ^19.2.8 + version: 19.2.8 react-dom: - specifier: ^19.2.7 - version: 19.2.7(react@19.2.7) + specifier: ^19.2.8 + version: 19.2.8(react@19.2.8) react-hook-form: - specifier: ^7.81.0 - version: 7.81.0(react@19.2.7) + specifier: ^7.85.0 + version: 7.85.0(react@19.2.8) react-i18next: - specifier: ^17.0.8 - version: 17.0.8(i18next@26.3.4(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + specifier: ^17.0.11 + version: 17.0.11(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.17)(react@19.2.7) + version: 10.1.0(@types/react@19.2.18)(react@19.2.8) react-router: - specifier: ^8.1.0 - version: 8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^8.3.0 + version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) sonner: - specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^2.0.8 + version: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) tailwind-merge: specifier: ^3.5.0 version: 3.6.0 tailwindcss: - specifier: ^4.3.2 - version: 4.3.2 + specifier: ^4.3.3 + version: 4.3.3 zod: specifier: ^4.3.6 version: 4.4.3 devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.6.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.1(jiti@2.7.0)) '@tanstack/eslint-plugin-query': - specifier: ^5.101.2 - version: 5.101.2(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + specifier: ^5.101.4 + version: 5.101.4(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) '@types/node': - specifier: ^26.1.0 - version: 26.1.0 + specifier: ^26.2.0 + version: 26.2.0 '@types/react': - specifier: ^19.2.17 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.4 + version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': - specifier: ^6.0.3 - version: 6.0.3(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0)) + specifier: ^6.0.5 + version: 6.0.5(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0)) eslint: - specifier: ^10.6.0 - version: 10.6.0(jiti@2.7.0) + specifier: ^10.8.1 + version: 10.8.1(jiti@2.7.0) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.1.1(eslint@10.6.0(jiti@2.7.0)) + version: 7.1.1(eslint@10.8.1(jiti@2.7.0)) eslint-plugin-react-refresh: specifier: ^0.5.3 - version: 0.5.3(eslint@10.6.0(jiti@2.7.0)) + version: 0.5.3(eslint@10.8.1(jiti@2.7.0)) globals: - specifier: ^17.7.0 - version: 17.7.0 + specifier: ^17.9.0 + version: 17.9.0 rollup-plugin-visualizer: specifier: ^7.0.1 - version: 7.0.1(rolldown@1.1.4) + version: 7.0.1(rolldown@1.2.3) tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -130,11 +130,11 @@ importers: specifier: ~6.0.2 version: 6.0.3 typescript-eslint: - specifier: ^8.62.1 - version: 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + specifier: ^8.66.0 + version: 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) vite: - specifier: ^8.1.3 - version: 8.1.3(@types/node@26.1.0)(jiti@2.7.0) + specifier: ^8.2.1 + version: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) packages: @@ -213,17 +213,8 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -236,8 +227,8 @@ packages: resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.2.1': @@ -261,25 +252,98 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - '@floating-ui/react-dom@2.1.8': - resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} - '@hookform/resolvers@5.4.0': - resolution: {integrity: sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==} + '@hookform/resolvers@5.7.1': + resolution: {integrity: sha512-8wS/P4UDr5sQDe4nFaV51TVyfDPrWgNIXweqG0Bs9Z5LSuzKLb+RQNPvkN2oHM5SRrJyWrVH/F+LOUcFjUyvwQ==} peerDependencies: + '@sinclair/typebox': '>=0.25.24' + '@standard-schema/spec': ^1.0.0 + '@typeschema/main': '>=0.13.7' + '@vinejs/vine': ^2.0.0 || ^3.0.0 || ^4.0.0 + ajv: ^8.12.0 + ajv-errors: ^3.0.0 + ajv-formats: ^2.1.1 + arktype: ^2.0.0 + ata-validator: ^1.2.0 + class-transformer: '>=0.4.0' + class-validator: '>=0.12.0' + computed-types: ^1.0.0 + effect: ^3.10.3 + fluentvalidation-ts: ^3.0.0 + fp-ts: ^2.7.0 + io-ts: ^2.0.0 + joi: ^17.0.0 + nope-validator: '>=0.12.0' react-hook-form: ^7.55.0 + superstruct: '>=0.12.0' + typanion: ^3.3.2 + valibot: '>=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc' + vest: '>=3.0.0' + yup: ^1.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@sinclair/typebox': + optional: true + '@standard-schema/spec': + optional: true + '@typeschema/main': + optional: true + '@vinejs/vine': + optional: true + ajv: + optional: true + ajv-errors: + optional: true + ajv-formats: + optional: true + arktype: + optional: true + ata-validator: + optional: true + class-transformer: + optional: true + class-validator: + optional: true + computed-types: + optional: true + effect: + optional: true + fluentvalidation-ts: + optional: true + fp-ts: + optional: true + io-ts: + optional: true + joi: + optional: true + nope-validator: + optional: true + superstruct: + optional: true + typanion: + optional: true + valibot: + optional: true + vest: + optional: true + yup: + optional: true + zod: + optional: true '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -317,23 +381,17 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} - '@oxc-project/types@0.138.0': - resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} + '@radix-ui/number@1.1.3': + resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} - '@radix-ui/number@1.1.2': - resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} + '@radix-ui/primitive@1.1.7': + resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} - '@radix-ui/primitive@1.1.4': - resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} - - '@radix-ui/react-accessible-icon@1.1.11': - resolution: {integrity: sha512-HQDOFTKwSnmUij6l54wYJJtxTAnxI71+YJLOrjm2ladFB8HAV5Jt7hwaZPhWTGBkYoW4+ZAOfNZrLDh/qvxSYA==} + '@radix-ui/react-accessible-icon@1.1.15': + resolution: {integrity: sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -345,8 +403,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-accordion@1.2.15': - resolution: {integrity: sha512-24Zz/0SYx8F2bSVThBnQrdJs2VbKelyuJordcFRRdA0fRAhrq/wSegGCqaQz34VQoiWqSMGYCYXEhynLSlyQlg==} + '@radix-ui/react-accordion@1.2.20': + resolution: {integrity: sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -358,8 +416,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-alert-dialog@1.1.18': - resolution: {integrity: sha512-6c2cXpNlAgHDhKguK24XcWHHayMpK+lk7/WwBXBco+ZJ4Dv7xP++GBM280KgTD/HCRu3jSdfe8WQiZssonYaIA==} + '@radix-ui/react-alert-dialog@1.1.23': + resolution: {integrity: sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -371,8 +429,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-arrow@1.1.11': - resolution: {integrity: sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==} + '@radix-ui/react-arrow@1.1.15': + resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -384,8 +442,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-aspect-ratio@1.1.11': - resolution: {integrity: sha512-IUAhIVpBUvP5NNICjlaB1OFmtRLGqQqTF3ZOSGPoq3XeLXRFtHiWTRxSVEULgOd9GQR2c7tsYqDnhUennapZnw==} + '@radix-ui/react-aspect-ratio@1.1.15': + resolution: {integrity: sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -397,8 +455,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-avatar@1.2.1': - resolution: {integrity: sha512-+8PWoLLZv3AVb5m0pvoiOca/bQGzc9vPVb+982HB2x3Un0DpYEPM3zLMl4oqRpBsocJuNqLkiv/HXTnTrlwr4g==} + '@radix-ui/react-avatar@1.2.6': + resolution: {integrity: sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -410,8 +468,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-checkbox@1.3.6': - resolution: {integrity: sha512-eUEUoGMDpfkgHWSE97ZZaUJtzR1M7EKnNIpD1Q16+8JR9NWghcaqMulx9PuCQ720w0UclfYn6FEbCdd5Hx087g==} + '@radix-ui/react-checkbox@1.3.11': + resolution: {integrity: sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -423,8 +481,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collapsible@1.1.15': - resolution: {integrity: sha512-8A1zibu5skAQ+UVbaeNH5hVMibiFCRJzgMuM14LTWGttnTZKQL9jwYnhAbHRuxrtCqPXa4JvvnVUq1pTNgyZYw==} + '@radix-ui/react-collapsible@1.1.20': + resolution: {integrity: sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -436,8 +494,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.11': - resolution: {integrity: sha512-djW9+zeg137KQdlPtmE8xnaD+K2rcXXMWFrSg0hsmYZ6HRbdTA7tDHFgpaW9+huWVEu0RCabL+985T4TA0BE7g==} + '@radix-ui/react-collection@1.1.15': + resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -449,8 +507,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-compose-refs@1.1.3': - resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -458,8 +516,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-context-menu@2.3.2': - resolution: {integrity: sha512-qzsA/ZPhF6yMxBOTIk1nlCkoy2mswSbwYL+ErBa2iP0s4WWrlxmczArYqMcpVfEjmM7KJj/ADPXky0yZfbSxtQ==} + '@radix-ui/react-context-menu@2.3.7': + resolution: {integrity: sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -471,8 +529,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-context@1.1.4': - resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} + '@radix-ui/react-context@1.2.2': + resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -480,8 +538,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.18': - resolution: {integrity: sha512-apa28mldjMgORmE6g/w3sCcA0Y9UAVeeDVoozN4i7kOw12mLl9RBchfzK3Nn6qxOWjrZhK1Lfy7f07kyzxtnBw==} + '@radix-ui/react-dialog@1.1.23': + resolution: {integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -493,8 +551,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-direction@1.1.2': - resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} + '@radix-ui/react-direction@1.1.4': + resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -502,8 +560,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.14': - resolution: {integrity: sha512-4lUhWTWAjbDIqFrAPWJ3WqBOpO5YchVZ88X3nh6H9Lu5AFi5nCUeTPj3D8FSDmabmFeRe9ME0BDA4MwKTha5GQ==} + '@radix-ui/react-dismissable-layer@1.1.19': + resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -515,8 +573,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.19': - resolution: {integrity: sha512-HZccBkbK0LOi8nYKIp5jll/zIRW0cCOmG6WWyqsSpmXCU+ZlcBbTqIwlBvPCu886C5RVu6c/kHV7xSP8IgYNHw==} + '@radix-ui/react-dropdown-menu@2.1.24': + resolution: {integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -528,8 +586,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-focus-guards@1.1.4': - resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} + '@radix-ui/react-focus-guards@1.1.6': + resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -537,8 +595,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-focus-scope@1.1.11': - resolution: {integrity: sha512-Mn88Vg2whaRocGJNOH+DKFqYm6ySFPQaiwHNxZPyjn99B52KAEJWWY9NP83+nWdk2HM3rdov+STu9AG471Rt9w==} + '@radix-ui/react-focus-scope@1.1.16': + resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -550,8 +608,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-form@0.1.11': - resolution: {integrity: sha512-0mTMJHv1gQAuEQoq5VDpTD3MRgmfUFdXAVFhpqR7wBeUr+tyRsof0wv/4XdPHLwQrefhoH2FiGHCggrCJhalIw==} + '@radix-ui/react-form@0.1.16': + resolution: {integrity: sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -563,8 +621,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-hover-card@1.1.18': - resolution: {integrity: sha512-rt+Fx4HoCeEwFL2IdoV2QaPltqDLlzxN77i9nwB3Y70scFlfAHh1QCdE2TXKuFJtA1TNygb0oivnFBZifgtZOw==} + '@radix-ui/react-hover-card@1.1.23': + resolution: {integrity: sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -576,8 +634,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-id@1.1.2': - resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -585,8 +643,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-label@2.1.11': - resolution: {integrity: sha512-3PKvDDxOn62k0oV1n4QtNtD2vpu+zYjXR7ojLBPaO6SPvhy53yg0vAmgNeBQeJW5rV3dffoRG+HYfLBZuzw0CQ==} + '@radix-ui/react-label@2.1.15': + resolution: {integrity: sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -598,8 +656,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menu@2.1.19': - resolution: {integrity: sha512-Mht9BVd1AIsNFVQr4KG3bIK7XQn5IXF0TL/2ObsrzOdc1loaly/+kBDL5roSCYn8j8XZkvpOD0WYLz2FQtH1Eg==} + '@radix-ui/react-menu@2.1.24': + resolution: {integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -611,8 +669,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menubar@1.1.19': - resolution: {integrity: sha512-Glt6mebxcgQvLeVkH3HiqV5bgQubE+31ELxLs7q0GlYI5k0XYkOkeuPrhXoylxK8eufvIt9CJjzY1TfFMXK3qw==} + '@radix-ui/react-menubar@1.1.24': + resolution: {integrity: sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -624,8 +682,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-navigation-menu@1.2.17': - resolution: {integrity: sha512-fYeYQvbeNn5AQk2RBbpO7koLm2YbS00UYxC/IL2sgLlninEH5UNIv+X3E0KJ1Vy4WIo+dhN9w8GNqSHhbHWCIg==} + '@radix-ui/react-navigation-menu@1.2.22': + resolution: {integrity: sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -637,8 +695,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-one-time-password-field@0.1.11': - resolution: {integrity: sha512-Rsgab65u73E5kPVh8OS6PgPwJgPyf08GFfJDGAbMdF4DL7CgDhFOaDnXuk/DiMEVF6kgQwl0oJmFklvipmiOLg==} + '@radix-ui/react-one-time-password-field@0.1.16': + resolution: {integrity: sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -650,8 +708,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-password-toggle-field@0.1.6': - resolution: {integrity: sha512-pQ3xGp/uemomASPH97Eb3shfXX8QlG11bBJyEvRBV+vwtO4HvQlS06Yj9f31Ao7XepvF98SFrRgVDQ7jv+2xjQ==} + '@radix-ui/react-password-toggle-field@0.1.11': + resolution: {integrity: sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -663,8 +721,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.18': - resolution: {integrity: sha512-qdXDes+eHlnMUGlBAAAe5EG7oOQvqsXuq4mq585diMudg80iB+jHbsSeG3+Q4eWNsogNyhqU2p/3i+Y0iEepqg==} + '@radix-ui/react-popover@1.1.23': + resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -676,8 +734,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popper@1.3.2': - resolution: {integrity: sha512-3QXNeMkdshed1MR3LNoiCirBywRFPkD8ETJa/HlPuLwSajaQixf2ro+isoDNJlGABg9ug41XuZpINZJIle4XWg==} + '@radix-ui/react-popper@1.3.7': + resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -689,8 +747,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-portal@1.1.13': - resolution: {integrity: sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==} + '@radix-ui/react-portal@1.1.17': + resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -702,8 +760,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.6': - resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} + '@radix-ui/react-presence@1.1.10': + resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -715,8 +773,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.7': - resolution: {integrity: sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==} + '@radix-ui/react-primitive@2.1.10': + resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -728,8 +786,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-progress@1.1.11': - resolution: {integrity: sha512-KqiGJcFaZDc+BvveAgU3ZhACg2MvSUDrCBx4lRR/ZVRNal0bvt8lBpvnSkep9heeOuF8Qfw3fszLDX4OpQ2NVw==} + '@radix-ui/react-progress@1.1.16': + resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -741,8 +799,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-radio-group@1.4.2': - resolution: {integrity: sha512-W8Uo9riHnlzLLWy+r2mVHUyuEWqD/+be4PZzbEvaGoFSBDHkm+GYWjtcE6u3AmPKNyfanWpnVfpZ2GqPCdzzsw==} + '@radix-ui/react-radio-group@1.4.7': + resolution: {integrity: sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -754,8 +812,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.14': - resolution: {integrity: sha512-8Qcnx9447tx/aCBgw6Jenfqg4Skq+vqab9mCBmuGNipIS5YXvL275wbKEu7+ICYHIlAPgCduUMJH1XOYewKF6Q==} + '@radix-ui/react-roving-focus@1.1.19': + resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -767,8 +825,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-scroll-area@1.2.13': - resolution: {integrity: sha512-7tncSubo2G0UY1e8rk+72qe3XRzrGnOLtZQ1PL1KoBfRUNX0NrJT5akb+0kfwSCc3gVR4wdHqyhAQBDpDNOwDw==} + '@radix-ui/react-scroll-area@1.2.18': + resolution: {integrity: sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -780,8 +838,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-select@2.3.2': - resolution: {integrity: sha512-brXD6C/V0fVK0DDbscLVw6LsXrjQ+ay8jdOBaN+tLb4vsHsAMm6Gt6eT77wHX1Eq8GPtD5rJ+RxFtfDozsb4+Q==} + '@radix-ui/react-select@2.3.7': + resolution: {integrity: sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -793,8 +851,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.11': - resolution: {integrity: sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==} + '@radix-ui/react-separator@1.1.15': + resolution: {integrity: sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -806,8 +864,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slider@1.4.2': - resolution: {integrity: sha512-qt5C1ppJz66aUDrH1VccjPrq7aFchK0wBrn6xsxlCHNUyE57dRRQ7lp1QFpF7OscMexZF8MCGBTVBlENHPkNiA==} + '@radix-ui/react-slider@1.4.7': + resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -819,8 +877,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.3.0': - resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} + '@radix-ui/react-slot@1.3.3': + resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -828,8 +886,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-switch@1.3.2': - resolution: {integrity: sha512-tgRBI3DdNwAJYE4BBZyZcz/HRRCvAsPkRvG1wvKc+41tBGMxPn/a87T/wikXAvyDypNQ9kaZwHbeZe+veHCGpA==} + '@radix-ui/react-switch@1.3.7': + resolution: {integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -841,8 +899,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tabs@1.1.16': - resolution: {integrity: sha512-v3Ab2l7z6U7tRB4xA0IyKdq0OsqaO1o9ZjsIEoKKnSZ/l96mZz8aCTX0NCXw+YVHJXr8Km4d+Mn6/Q8YjXa+gw==} + '@radix-ui/react-tabs@1.1.21': + resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -854,8 +912,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toast@1.2.18': - resolution: {integrity: sha512-YNEnTHV47hPep+U0QvVM02OJNka9uygREc+k4Nh5VSZBg4MmE+myI442x3hCGfRpX7N2WSSYSJKws4gE+Z8lgg==} + '@radix-ui/react-toast@1.2.23': + resolution: {integrity: sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -867,8 +925,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toggle-group@1.1.14': - resolution: {integrity: sha512-TK1vusNKb8IRhF23FTbRgUNZ9zfs5rGIyI7LfR3h26p9LrQ060i0uW9QWeD8baZMddaaP0DBGlIa6pbZG+mitg==} + '@radix-ui/react-toggle-group@1.1.19': + resolution: {integrity: sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -880,8 +938,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toggle@1.1.13': - resolution: {integrity: sha512-bI2ILJrzwgmAsH05TsJ9pVrzqQwAip7OM2/krqAdYn0R16bl86UPWbe5VPHsALat0EnqpV01cGtkleaUKPNdNg==} + '@radix-ui/react-toggle@1.1.18': + resolution: {integrity: sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -893,8 +951,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-toolbar@1.1.14': - resolution: {integrity: sha512-L/EkWVqlnj3lL2toHh4C7PwH2jxfa7OCq6lGfXSCii99ve2S4Ux5rc9HnOa7LN9exHa/Nl9kmCAmP9BuDPy5UA==} + '@radix-ui/react-toolbar@1.1.19': + resolution: {integrity: sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -906,8 +964,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tooltip@1.2.11': - resolution: {integrity: sha512-8XZ6Py3y3W2nEzAUGCN5cfVKaUi+CVApcz1d6lrNVVf2hvYEixMRkq8k9ggPKnQUpRRuOV5avt8uvxViH2jLwA==} + '@radix-ui/react-tooltip@1.2.16': + resolution: {integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -919,8 +977,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-use-callback-ref@1.1.2': - resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} + '@radix-ui/react-use-callback-ref@1.1.4': + resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -928,8 +986,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-controllable-state@1.2.3': - resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} + '@radix-ui/react-use-controllable-state@1.2.6': + resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -937,8 +995,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-effect-event@0.0.3': - resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -946,8 +1004,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-escape-keydown@1.1.3': - resolution: {integrity: sha512-3wEkMiPHXha/2VadZ68rYBcmYnPINVGl4Y3gtcM7fKRjANk0OscK+cdqBgUWdozb7YJxsh0vefM7vgAMHXOjqg==} + '@radix-ui/react-use-escape-keydown@1.1.5': + resolution: {integrity: sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -955,8 +1013,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-is-hydrated@0.1.1': - resolution: {integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==} + '@radix-ui/react-use-is-hydrated@0.1.3': + resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -964,8 +1022,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-layout-effect@1.1.2': - resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -973,8 +1031,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-previous@1.1.2': - resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} + '@radix-ui/react-use-previous@1.1.4': + resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -982,8 +1040,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-rect@1.1.2': - resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -991,8 +1049,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-size@1.1.2': - resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} + '@radix-ui/react-use-size@1.1.4': + resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -1000,8 +1058,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-visually-hidden@1.2.7': - resolution: {integrity: sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==} + '@radix-ui/react-visually-hidden@1.2.11': + resolution: {integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1013,100 +1071,95 @@ packages: '@types/react-dom': optional: true - '@radix-ui/rect@1.1.2': - resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} - '@rolldown/binding-android-arm64@1.1.4': - resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.4': - resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.4': - resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.4': - resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.4': - resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.4': - resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.4': - resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.4': - resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.4': - resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.4': - resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.4': - resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.4': - resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.4': - resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.4': - resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.4': - resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1117,69 +1170,69 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@tailwindcss/node@4.3.2': - resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - '@tailwindcss/oxide-android-arm64@4.3.2': - resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.2': - resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.2': - resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.2': - resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': - resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': - resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.2': - resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.2': - resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.2': - resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.2': - resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1190,29 +1243,29 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': - resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.2': - resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.2': - resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.3.2': - resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==} + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tanstack/eslint-plugin-query@5.101.2': - resolution: {integrity: sha512-cPE99s3XZwlObfn8lCezT4j4JLj2CVzpIEywx0H4hzfPsX/o9QhdwaOwcDXxrQAqx2ds7TbvTinxhB8B/ywb6w==} + '@tanstack/eslint-plugin-query@5.101.4': + resolution: {integrity: sha512-jqAZJWXGd5PthJYH9wmC2O5Ry5xAN2/7zxi9qcANQ+Xix8V5JkqNRjZ8Fh+rYzqzVYGiHqbrHekt+uh38OZVpw==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ^5.4.0 || ^6.0.0 @@ -1220,17 +1273,14 @@ packages: typescript: optional: true - '@tanstack/query-core@5.101.2': - resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} - '@tanstack/react-query@5.101.2': - resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} + '@tanstack/react-query@5.101.4': + resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==} peerDependencies: react: ^18 || ^19 - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -1255,16 +1305,16 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@26.1.0': - resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1272,70 +1322,107 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.62.1': - resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} + '@typescript-eslint/eslint-plugin@8.66.0': + resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.62.1 + '@typescript-eslint/parser': ^8.66.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.62.1': - resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} + '@typescript-eslint/parser@8.66.0': + resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.62.1': - resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.62.1': - resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.62.1': - resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} + '@typescript-eslint/project-service@8.66.0': + resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.62.1': - resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.66.0': + resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/tsconfig-utils@8.66.0': + resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.66.0': + resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.62.1': - resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.62.1': - resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} + '@typescript-eslint/types@8.66.0': + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.62.1': - resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} + '@typescript-eslint/typescript-estree@8.66.0': + resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.62.1': - resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} + '@typescript-eslint/utils@8.66.0': + resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.66.0': + resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@vitejs/plugin-react@6.0.3': - resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} + '@vitejs/plugin-react@6.0.5': + resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -1352,8 +1439,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -1379,8 +1466,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.18.1: - resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1394,9 +1481,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} @@ -1515,8 +1602,8 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - enhanced-resolve@5.21.6: - resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} + enhanced-resolve@5.24.4: + resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==} engines: {node: '>=10.13.0'} es-define-property@1.0.1: @@ -1566,8 +1653,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.6.0: - resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + eslint@10.8.1: + resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1632,8 +1719,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} @@ -1684,8 +1771,8 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - globals@17.7.0: - resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + globals@17.9.0: + resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} gopd@1.2.0: @@ -1719,8 +1806,8 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - html-parse-stringify@3.0.1: - resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-parse-stringify@4.0.1: + resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==} html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -1732,13 +1819,13 @@ packages: i18next-browser-languagedetector@8.2.1: resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} - i18next-resources-to-backend@1.2.1: - resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} + i18next-resources-to-backend@1.2.3: + resolution: {integrity: sha512-8Y/LLAm5fqZc2ckQxtTWbu75ndjNLzF7mcYl6rhc4g+WBopIsG3YhdYqF5qhJy64tlYhlU2KSSnEgW3gGHLqwg==} - i18next@26.3.4: - resolution: {integrity: sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==} + i18next@26.3.6: + resolution: {integrity: sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==} peerDependencies: - typescript: ^5 || ^6 + typescript: ^5 || ^6 || ^7 peerDependenciesMeta: typescript: optional: true @@ -1747,8 +1834,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} imurmurhash@0.1.4: @@ -1842,30 +1929,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -1873,6 +1990,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -1880,6 +2004,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -1887,6 +2018,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -1894,22 +2032,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1920,8 +2081,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@1.23.0: - resolution: {integrity: sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==} + lucide-react@1.31.0: + resolution: {integrity: sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2027,15 +2188,15 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2089,8 +2250,8 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} - postcss@8.5.16: - resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} powershell-utils@0.1.0: @@ -2112,8 +2273,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - radix-ui@1.6.1: - resolution: {integrity: sha512-QXDXJtB6sK83mLASONYUZCauatcWb+knFviFpN1EhtdbbmlsRmzCLrbZSKztnNiem2KOHIBbiDbauVB7SORXMw==} + radix-ui@1.6.7: + resolution: {integrity: sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2125,25 +2286,25 @@ packages: '@types/react-dom': optional: true - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: - react: ^19.2.7 + react: ^19.2.8 - react-hook-form@7.81.0: - resolution: {integrity: sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==} + react-hook-form@7.85.0: + resolution: {integrity: sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@17.0.8: - resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} + react-i18next@17.0.11: + resolution: {integrity: sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==} peerDependencies: i18next: '>= 26.2.0' react: '>= 16.8.0' react-dom: '*' react-native: '*' - typescript: ^5 || ^6 + typescript: ^5 || ^6 || ^7 peerDependenciesMeta: react-dom: optional: true @@ -2178,8 +2339,8 @@ packages: '@types/react': optional: true - react-router@8.1.0: - resolution: {integrity: sha512-Mdfi61uObuvWNN9OhChOC0HV6YWOIfKRzEWOvCHRSuQg8IM+Nv10edaM/2HE8ZixBpUTdQbruyWqC3sDkkh9vw==} + react-router@8.3.0: + resolution: {integrity: sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==} engines: {node: '>=22.22.0'} peerDependencies: react: '>=19.2.7' @@ -2198,8 +2359,8 @@ packages: '@types/react': optional: true - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} remark-parse@11.0.0: @@ -2208,8 +2369,8 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - rolldown@1.1.4: - resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2250,11 +2411,15 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - sonner@2.0.7: - resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + sonner@2.0.8: + resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==} peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -2287,8 +2452,8 @@ packages: tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwindcss@4.3.2: - resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -2320,8 +2485,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.62.1: - resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} + typescript-eslint@8.66.0: + resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2393,13 +2558,13 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@8.1.3: - resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -2436,10 +2601,6 @@ packages: yaml: optional: true - void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2594,25 +2755,9 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@emnapi/core@1.11.1': + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1(jiti@2.7.0))': dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.7.0))': - dependencies: - eslint: 10.6.0(jiti@2.7.0) + eslint: 10.8.1(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2621,11 +2766,11 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.6.0': + '@eslint/config-helpers@0.7.0': dependencies: '@eslint/core': 1.2.1 @@ -2633,9 +2778,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.6.0(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.8.1(jiti@2.7.0))': optionalDependencies: - eslint: 10.6.0(jiti@2.7.0) + eslint: 10.8.1(jiti@2.7.0) '@eslint/object-schema@3.0.5': {} @@ -2644,27 +2789,29 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@floating-ui/core@1.7.5': + '@floating-ui/core@1.8.0': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.6': + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@floating-ui/react-dom@2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@floating-ui/dom': 1.7.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@floating-ui/dom': 1.8.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.12': {} - '@hookform/resolvers@5.4.0(react-hook-form@7.81.0(react@19.2.7))': + '@hookform/resolvers@5.7.1(react-hook-form@7.85.0(react@19.2.8))(zod@4.4.3)': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.81.0(react@19.2.7) + react-hook-form: 7.85.0(react@19.2.8) + optionalDependencies: + zod: 4.4.3 '@humanfs/core@0.19.2': dependencies: @@ -2701,900 +2848,884 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@oxc-project/types@0.143.0': {} + + '@radix-ui/number@1.1.3': {} + + '@radix-ui/primitive@1.1.7': {} + + '@radix-ui/react-accessible-icon@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.138.0': {} - - '@radix-ui/number@1.1.2': {} - - '@radix-ui/primitive@1.1.4': {} - - '@radix-ui/react-accessible-icon@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-accordion@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collapsible': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-alert-dialog@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-alert-dialog@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-arrow@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-aspect-ratio@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-aspect-ratio@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-avatar@1.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-avatar@1.2.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-checkbox@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-checkbox@1.3.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-collapsible@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-collection@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-context-menu@2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-context-menu@2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-context@1.2.2(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-dialog@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-direction@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-dismissable-layer@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-dropdown-menu@2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-focus-scope@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-form@0.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-form@0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-label': 2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-hover-card@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-hover-card@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-id@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-label@2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-label@2.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-menu@2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-menu@2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-menubar@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-menubar@1.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-navigation-menu@1.2.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-one-time-password-field@0.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-one-time-password-field@0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/number': 1.1.2 - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-password-toggle-field@0.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-password-toggle-field@0.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-popover@1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-popper@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/rect': 1.1.2 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/rect': 1.1.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-portal@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-primitive@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-progress@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-progress@1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-radio-group@1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-radio-group@1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-roving-focus@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-scroll-area@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/number': 1.1.2 - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-select@2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-select@2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/number': 1.1.2 - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-separator@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-separator@1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-slider@1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-slider@1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/number': 1.1.2 - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-slot@1.3.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-switch@1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-switch@1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-tabs@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-toast@1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-toast@1.2.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-toggle-group@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-toggle-group@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-toggle': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-toggle@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-toggle@1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-toolbar@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-toolbar@1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-separator': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-toggle-group': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-tooltip@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-escape-keydown@1.1.3(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-escape-keydown@1.1.5(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-previous@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/rect': 1.1.2 - react: 19.2.7 + '@radix-ui/rect': 1.1.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-visually-hidden@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - '@radix-ui/rect@1.1.2': {} + '@radix-ui/rect@1.1.3': {} - '@rolldown/binding-android-arm64@1.1.4': + '@rolldown/binding-android-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-arm64@1.1.4': + '@rolldown/binding-darwin-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-x64@1.1.4': + '@rolldown/binding-darwin-x64@1.2.3': optional: true - '@rolldown/binding-freebsd-x64@1.1.4': + '@rolldown/binding-freebsd-x64@1.2.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.4': + '@rolldown/binding-linux-arm64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.4': + '@rolldown/binding-linux-arm64-musl@1.2.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.4': + '@rolldown/binding-linux-ppc64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.4': + '@rolldown/binding-linux-s390x-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.4': + '@rolldown/binding-linux-x64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-musl@1.1.4': + '@rolldown/binding-linux-x64-musl@1.2.3': optional: true - '@rolldown/binding-openharmony-arm64@1.1.4': + '@rolldown/binding-openharmony-arm64@1.2.3': optional: true - '@rolldown/binding-wasm32-wasi@1.1.4': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-win32-arm64-msvc@1.2.3': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.4': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.1.4': + '@rolldown/binding-win32-x64-msvc@1.2.3': optional: true '@rolldown/pluginutils@1.0.1': {} '@standard-schema/utils@0.3.0': {} - '@tailwindcss/node@4.3.2': + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.21.6 + enhanced-resolve: 5.24.4 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.2 + tailwindcss: 4.3.3 - '@tailwindcss/oxide-android-arm64@4.3.2': + '@tailwindcss/oxide-android-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.2': + '@tailwindcss/oxide-darwin-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.2': + '@tailwindcss/oxide-darwin-x64@4.3.3': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.2': + '@tailwindcss/oxide-freebsd-x64@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.2': + '@tailwindcss/oxide-linux-x64-musl@4.3.3': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.2': + '@tailwindcss/oxide-wasm32-wasi@4.3.3': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': optional: true - '@tailwindcss/oxide@4.3.2': + '@tailwindcss/oxide@4.3.3': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.2 - '@tailwindcss/oxide-darwin-arm64': 4.3.2 - '@tailwindcss/oxide-darwin-x64': 4.3.2 - '@tailwindcss/oxide-freebsd-x64': 4.3.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 - '@tailwindcss/oxide-linux-x64-musl': 4.3.2 - '@tailwindcss/oxide-wasm32-wasi': 4.3.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.2(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))': dependencies: - '@tailwindcss/node': 4.3.2 - '@tailwindcss/oxide': 4.3.2 - tailwindcss: 4.3.2 - vite: 8.1.3(@types/node@26.1.0)(jiti@2.7.0) + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - '@tanstack/eslint-plugin-query@5.101.2(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@tanstack/eslint-plugin-query@5.101.4(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.6.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.65.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@tanstack/query-core@5.101.2': {} + '@tanstack/query-core@5.101.4': {} - '@tanstack/react-query@5.101.2(react@19.2.7)': + '@tanstack/react-query@5.101.4(react@19.2.8)': dependencies: - '@tanstack/query-core': 5.101.2 - react: 19.2.7 - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true + '@tanstack/query-core': 5.101.4 + react: 19.2.8 '@types/debug@4.1.13': dependencies: @@ -3620,15 +3751,15 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@26.1.0': + '@types/node@26.2.0': dependencies: undici-types: 8.3.0 - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -3636,74 +3767,94 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.62.1 - eslint: 10.6.0(jiti@2.7.0) - ignore: 7.0.5 + '@typescript-eslint/parser': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 + eslint: 10.8.1(jiti@2.7.0) + ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.62.1 + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3 - eslint: 10.6.0(jiti@2.7.0) + eslint: 10.8.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)': + '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) - '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.62.1': + '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/visitor-keys': 8.62.1 + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)': + '@typescript-eslint/scope-manager@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + + '@typescript-eslint/scope-manager@8.66.0': + dependencies: + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 + + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.6.0(jiti@2.7.0) + eslint: 10.8.1(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.62.1': {} + '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)': + '@typescript-eslint/types@8.66.0': {} + + '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/visitor-keys': 8.62.1 + '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3711,34 +3862,65 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - eslint: 10.6.0(jiti@2.7.0) + '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.62.1': + '@typescript-eslint/utils@8.65.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.62.1 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + eslint-visitor-keys: 5.0.1 + + '@typescript-eslint/visitor-keys@8.66.0': + dependencies: + '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.1': {} - '@vitejs/plugin-react@6.0.3(vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0))': + '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.3(@types/node@26.1.0)(jiti@2.7.0) + vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn@8.17.0: {} + acorn@8.18.0: {} agent-base@6.0.2: dependencies: @@ -3763,7 +3945,7 @@ snapshots: asynckit@0.4.0: {} - axios@1.18.1: + axios@1.19.0: dependencies: follow-redirects: 1.16.0 form-data: 4.0.6 @@ -3779,7 +3961,7 @@ snapshots: baseline-browser-mapping@2.10.29: {} - brace-expansion@5.0.7: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -3883,7 +4065,7 @@ snapshots: emoji-regex@10.6.0: {} - enhanced-resolve@5.21.6: + enhanced-resolve@5.24.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -3907,20 +4089,20 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@7.1.1(eslint@10.6.0(jiti@2.7.0)): + eslint-plugin-react-hooks@7.1.1(eslint@10.8.1(jiti@2.7.0)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.3 - eslint: 10.6.0(jiti@2.7.0) + eslint: 10.8.1(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.3(eslint@10.6.0(jiti@2.7.0)): + eslint-plugin-react-refresh@0.5.3(eslint@10.8.1(jiti@2.7.0)): dependencies: - eslint: 10.6.0(jiti@2.7.0) + eslint: 10.8.1(jiti@2.7.0) eslint-scope@9.1.2: dependencies: @@ -3933,12 +4115,12 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.6.0(jiti@2.7.0): + eslint@10.8.1(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.6.0 + '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 @@ -3962,7 +4144,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -3972,8 +4154,8 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esquery@1.7.0: @@ -4013,10 +4195,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.4 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.4.4: {} follow-redirects@1.16.0: {} @@ -4063,7 +4245,7 @@ snapshots: dependencies: is-glob: 4.0.3 - globals@17.7.0: {} + globals@17.9.0: {} gopd@1.2.0: {} @@ -4109,9 +4291,7 @@ snapshots: dependencies: hermes-estree: 0.25.1 - html-parse-stringify@3.0.1: - dependencies: - void-elements: 3.1.0 + html-parse-stringify@4.0.1: {} html-url-attributes@3.0.1: {} @@ -4126,17 +4306,17 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - i18next-resources-to-backend@1.2.1: + i18next-resources-to-backend@1.2.3: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 - i18next@26.3.4(typescript@6.0.3): + i18next@26.3.6(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.6: {} imurmurhash@0.1.4: {} @@ -4201,36 +4381,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -4247,6 +4460,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4257,9 +4486,9 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@1.23.0(react@19.2.7): + lucide-react@1.31.0(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 magic-string@0.30.21: dependencies: @@ -4495,20 +4724,20 @@ snapshots: dependencies: mime-db: 1.52.0 - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.9 ms@2.1.3: {} - nanoid@3.3.15: {} + nanoid@3.3.18: {} natural-compare@1.4.0: {} - next-themes@0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) node-releases@2.0.44: {} @@ -4558,9 +4787,9 @@ snapshots: picomatch@4.0.5: {} - postcss@8.5.16: + postcss@8.5.26: dependencies: - nanoid: 3.3.15 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -4574,99 +4803,99 @@ snapshots: punycode@2.3.1: {} - radix-ui@1.6.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + radix-ui@1.6.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-accessible-icon': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-accordion': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-alert-dialog': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-aspect-ratio': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-avatar': 1.2.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-checkbox': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-collapsible': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context-menu': 2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-dialog': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-dropdown-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-form': 0.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-hover-card': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-label': 2.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-menubar': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-navigation-menu': 1.2.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-one-time-password-field': 0.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-password-toggle-field': 0.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-popover': 1.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-progress': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-radio-group': 1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-scroll-area': 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-select': 2.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-separator': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slider': 1.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-switch': 1.3.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-tabs': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-toast': 1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-toggle': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-toggle-group': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-toolbar': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-tooltip': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-escape-keydown': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@radix-ui/primitive': 1.1.7 + '@radix-ui/react-accessible-icon': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-alert-dialog': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-aspect-ratio': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-avatar': 1.2.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context-menu': 2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dropdown-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-form': 0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-hover-card': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-label': 2.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-menubar': 1.1.24(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-one-time-password-field': 0.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-password-toggle-field': 0.1.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-progress': 1.1.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-radio-group': 1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-select': 2.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slider': 1.4.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-switch': 1.3.7(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toast': 1.2.23(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-toolbar': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-escape-keydown': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) - react-dom@19.2.7(react@19.2.7): + react-dom@19.2.8(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 scheduler: 0.27.0 - react-hook-form@7.81.0(react@19.2.7): + react-hook-form@7.85.0(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 - react-i18next@17.0.8(i18next@26.3.4(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3): + react-i18next@17.0.11(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 - html-parse-stringify: 3.0.1 - i18next: 26.3.4(typescript@6.0.3) - react: 19.2.7 - use-sync-external-store: 1.6.0(react@19.2.7) + html-parse-stringify: 4.0.1 + i18next: 26.3.6(typescript@6.0.3) + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8) optionalDependencies: - react-dom: 19.2.7(react@19.2.7) + react-dom: 19.2.8(react@19.2.8) typescript: 6.0.3 - react-markdown@10.1.0(@types/react@19.2.17)(react@19.2.7): + react-markdown@10.1.0(@types/react@19.2.18)(react@19.2.8): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.17 + '@types/react': 19.2.18 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.1 - react: 19.2.7 + react: 19.2.8 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -4675,41 +4904,41 @@ snapshots: transitivePeerDependencies: - supports-color - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): + react-remove-scroll-bar@2.3.8(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.7 - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.8 + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): + react-remove-scroll@2.7.2(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.7 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.8 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.18)(react@19.2.8) + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) - use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) + use-callback-ref: 1.3.3(@types/react@19.2.18)(react@19.2.8) + use-sidecar: 1.1.3(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: cookie-es: 3.1.1 - react: 19.2.7 + react: 19.2.8 optionalDependencies: - react-dom: 19.2.7(react@19.2.7) + react-dom: 19.2.8(react@19.2.8) - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): + react-style-singleton@2.2.3(@types/react@19.2.18)(react@19.2.8): dependencies: get-nonce: 1.0.1 - react: 19.2.7 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react@19.2.7: {} + react@19.2.8: {} remark-parse@11.0.0: dependencies: @@ -4728,35 +4957,34 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - rolldown@1.1.4: + rolldown@1.2.3: dependencies: - '@oxc-project/types': 0.138.0 + '@oxc-project/types': 0.143.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.4 - '@rolldown/binding-darwin-arm64': 1.1.4 - '@rolldown/binding-darwin-x64': 1.1.4 - '@rolldown/binding-freebsd-x64': 1.1.4 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 - '@rolldown/binding-linux-arm64-gnu': 1.1.4 - '@rolldown/binding-linux-arm64-musl': 1.1.4 - '@rolldown/binding-linux-ppc64-gnu': 1.1.4 - '@rolldown/binding-linux-s390x-gnu': 1.1.4 - '@rolldown/binding-linux-x64-gnu': 1.1.4 - '@rolldown/binding-linux-x64-musl': 1.1.4 - '@rolldown/binding-openharmony-arm64': 1.1.4 - '@rolldown/binding-wasm32-wasi': 1.1.4 - '@rolldown/binding-win32-arm64-msvc': 1.1.4 - '@rolldown/binding-win32-x64-msvc': 1.1.4 + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 - rollup-plugin-visualizer@7.0.1(rolldown@1.1.4): + rollup-plugin-visualizer@7.0.1(rolldown@1.2.3): dependencies: open: 11.0.0 picomatch: 4.0.4 source-map: 0.7.6 yargs: 18.0.0 optionalDependencies: - rolldown: 1.1.4 + rolldown: 1.2.3 run-applescript@7.1.0: {} @@ -4772,10 +5000,12 @@ snapshots: shebang-regex@3.0.0: {} - sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 source-map-js@1.2.1: {} @@ -4808,7 +5038,7 @@ snapshots: tailwind-merge@3.6.0: {} - tailwindcss@4.3.2: {} + tailwindcss@4.3.3: {} tapable@2.3.3: {} @@ -4833,13 +5063,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.6.0(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.8.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4891,24 +5121,24 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): + use-callback-ref@1.3.3(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): + use-sidecar@1.1.3(@types/react@19.2.18)(react@19.2.8): dependencies: detect-node-es: 1.1.0 - react: 19.2.7 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - use-sync-external-store@1.6.0(react@19.2.7): + use-sync-external-store@1.6.0(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 vfile-message@4.0.3: dependencies: @@ -4920,20 +5150,18 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.1.3(@types/node@26.1.0)(jiti@2.7.0): + vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0): dependencies: - lightningcss: 1.32.0 + lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.16 - rolldown: 1.1.4 + postcss: 8.5.26 + rolldown: 1.2.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.2.0 fsevents: 2.3.3 jiti: 2.7.0 - void-elements@3.1.0: {} - which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/frontend/src/components/quick-actions/quick-actions.tsx b/frontend/src/components/quick-actions/quick-actions.tsx index c141f616..cc33b2f4 100644 --- a/frontend/src/components/quick-actions/quick-actions.tsx +++ b/frontend/src/components/quick-actions/quick-actions.tsx @@ -32,8 +32,8 @@ import { useTranslation } from "react-i18next"; import { useLocation } from "react-router"; import { useRef } from "react"; import { + searchParamsFromObject, useScreenParams, - recompileScreenParams, } from "@/lib/hooks/screen-params"; import { useMutation } from "@tanstack/react-query"; import axios from "axios"; @@ -70,7 +70,13 @@ export const QuickActions = () => { const redirectTimer = useRef(null); const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); - const compiledParams = recompileScreenParams(screenParams); + const compiledParams = (() => { + const params = searchParamsFromObject(screenParams).toString(); + if (params.length > 0) { + return `?${params}`; + } + return ""; + })(); const [isOpen, setIsOpen] = useState(false); diff --git a/frontend/src/lib/hooks/login-for.ts b/frontend/src/lib/hooks/login-for.ts index 8cf11579..b106c5d7 100644 --- a/frontend/src/lib/hooks/login-for.ts +++ b/frontend/src/lib/hooks/login-for.ts @@ -1,10 +1,11 @@ type UseLoginForProps = { login_for?: "oidc" | "app"; - compiledParams: string; + params: URLSearchParams; }; export const useLoginFor = (props: UseLoginForProps): string => { - const { login_for, compiledParams } = props; + const { login_for, params } = props; + const compiledParams = params.toString() ? "?" + params.toString() : ""; switch (login_for) { case "oidc": @@ -12,6 +13,9 @@ export const useLoginFor = (props: UseLoginForProps): string => { case "app": return "/continue" + compiledParams; default: + if (params.get("redirect_uri")) { + return "/continue" + compiledParams + } return "/logout"; } }; diff --git a/frontend/src/lib/hooks/redirect-uri.ts b/frontend/src/lib/hooks/redirect-uri.ts index c4fc9a12..286773be 100644 --- a/frontend/src/lib/hooks/redirect-uri.ts +++ b/frontend/src/lib/hooks/redirect-uri.ts @@ -75,18 +75,21 @@ export const useRedirectUri = ( }; }; -// ported from internal/controller/oauth_controller.go -const getEffectivePort = (url: URL): string => { - if (url.port) { - return url.port; - } +// https://www.geeksforgeeks.org/javascript/how-to-check-if-a-string-is-a-valid-ip-address-format-in-javascript +const isIP = (str: string): boolean => { + const ipv4 = + /^(\d{1,3}\.){3}\d{1,3}$/; + const ipv6 = + /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; + return ipv4.test(str) || ipv6.test(str) || str.startsWith("["); +} - if (url.protocol == "https:") { - return "443"; +const trimPeriod = (str: string): string => { + if(str.lastIndexOf('.') === (str.length - 1)){ + str = str.substring(0, str.length - 1); } - - return "80"; -}; + return str +} export const isTrustedDomain = ( url: URL, @@ -94,15 +97,15 @@ export const isTrustedDomain = ( cookieDomain: string, subdomainsEnabled: boolean, ): boolean => { - if (url.protocol != appUrl.protocol) { + if (isIP(url.hostname)) { return false; } - if (getEffectivePort(url) != getEffectivePort(appUrl)) { + if (url.port != appUrl.port) { return false; } - if (url.hostname == appUrl.hostname) { + if (trimPeriod(url.hostname) == trimPeriod(appUrl.hostname)) { return true; } @@ -110,9 +113,6 @@ export const isTrustedDomain = ( return false; } - if (url.hostname.endsWith("." + cookieDomain.toLowerCase())) { - return true; - } - - return false; + return trimPeriod(url.hostname).endsWith("." + cookieDomain.toLowerCase()) + || trimPeriod(url.hostname) == cookieDomain.toLowerCase(); }; diff --git a/frontend/src/lib/hooks/screen-params.ts b/frontend/src/lib/hooks/screen-params.ts index abf3a41a..6a459733 100644 --- a/frontend/src/lib/hooks/screen-params.ts +++ b/frontend/src/lib/hooks/screen-params.ts @@ -27,16 +27,10 @@ export function useScreenParams(params: URLSearchParams): ScreenParams { return parsed.data; } -export function recompileScreenParams(params: ScreenParams): string { - const p = new URLSearchParams( +export function searchParamsFromObject(obj: object): URLSearchParams { + return new URLSearchParams( Object.fromEntries( - Object.entries(params).filter(([, v]) => v !== undefined), + Object.entries(obj).filter(([, v]) => v !== undefined), ) as Record, - ).toString(); - - if (p.length > 0) { - return "?" + p; - } - - return ""; + ); } diff --git a/frontend/src/lib/i18n/locales/cs-CZ.json b/frontend/src/lib/i18n/locales/cs-CZ.json index d05edbfb..e883658b 100644 --- a/frontend/src/lib/i18n/locales/cs-CZ.json +++ b/frontend/src/lib/i18n/locales/cs-CZ.json @@ -15,16 +15,16 @@ "loginOauthSuccessTitle": "Přesměrování", "loginOauthSuccessSubtitle": "Přesměrování k poskytovateli OAuth", "loginOauthAutoRedirectTitle": "Automatické přesměrování OAuth", - "loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.", - "loginOauthAutoRedirectButton": "Redirect now", + "loginOauthAutoRedirectSubtitle": "Pro ověření budete automaticky přesměrování na svého poskytovatele OAuth.", + "loginOauthAutoRedirectButton": "Přesměrovat nyní", "continueTitle": "Pokračovat", "continueRedirectingTitle": "Přesměrování...", "continueRedirectingSubtitle": "Brzy budete přesměrováni do aplikace", - "continueRedirectManually": "Redirect me manually", + "continueRedirectManually": "Přesměrovat ručně", "continueInsecureRedirectTitle": "Nezabezpečené přesměrování", "continueInsecureRedirectSubtitle": "Pokoušíte se přesměrovat z https na http, které není bezpečné. Opravdu chcete pokračovat?", - "continueUntrustedRedirectTitle": "Untrusted redirect", - "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{cookieDomain}}). Are you sure you want to continue?", + "continueUntrustedRedirectTitle": "Nedůvěryhodné přesměrování", + "continueUntrustedRedirectSubtitle": "Pokoušíte se přesměrovat na doménu, která neodpovídá vaší nakonfigurované doméně ({{cookieDomain}}). Opravdu chcete pokračovat?", "logoutFailTitle": "Odhlášení se nezdařilo", "logoutFailSubtitle": "Zkuste to prosím znovu", "logoutSuccessTitle": "Odhlášen", @@ -51,53 +51,53 @@ "forgotPasswordTitle": "Zapomněli jste heslo?", "failedToFetchProvidersTitle": "Nepodařilo se načíst poskytovatele ověřování. Zkontrolujte prosím konfiguraci.", "errorTitle": "Došlo k chybě", - "errorSubtitleInfo": "The following error occurred while processing your request:", + "errorSubtitleInfo": "Při zpracování požadavku došlo k následující chybě:", "errorSubtitle": "Nastala chyba při pokusu o provedení této akce. Pro více informací prosím zkontrolujte konzolu.", "forgotPasswordMessage": "Heslo můžete obnovit změnou proměnné `USERS`.", "fieldRequired": "Toto pole je povinné", "invalidInput": "Neplatný údaj", - "domainWarningTitle": "Invalid Domain", - "domainWarningSubtitle": "You are accessing this instance from an incorrect domain. If you proceed, you may encounter issues with authentication.", - "domainWarningCurrent": "Current:", - "domainWarningExpected": "Expected:", - "ignoreTitle": "Ignore", - "goToCorrectDomainTitle": "Go to correct domain", - "authorizeTitle": "Authorize", - "authorizeCardTitle": "Continue to {{app}}?", - "authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.", - "authorizeSubtitleOAuth": "Would you like to continue to this app?", - "authorizeLoadingTitle": "Loading...", + "domainWarningTitle": "Neplatná doména", + "domainWarningSubtitle": "Pro přístup k této instanci používáte špatnou doménu. Při pokračování mohou nastat problémy s autentizací.", + "domainWarningCurrent": "Aktuální:", + "domainWarningExpected": "Vyžadovaná:", + "ignoreTitle": "Ignorovat", + "goToCorrectDomainTitle": "Přejít na správnou doménu", + "authorizeTitle": "Autorizovat", + "authorizeCardTitle": "Pokračovat do {{app}}?", + "authorizeSubtitle": "Chcete pokračovat do této aplikace? Pečlivě si zkontrolujte jí vyžadovaná oprávnění, prosím.", + "authorizeSubtitleOAuth": "Chcete pokračovat do této aplikace?", + "authorizeLoadingTitle": "Načítání...", "authorizeLoadingSubtitle": "Please wait while we load the client information.", - "authorizeSuccessTitle": "Authorized", - "authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", - "authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.", - "authorizeErrorInvalidParams": "The request is missing required parameters or has invalid parameters. Please check the URL and try again.", + "authorizeSuccessTitle": "Autorizováno", + "authorizeSuccessSubtitle": "Během několika vteřin budete přesměrováni do aplikace.", + "authorizeErrorClientInfo": "Při načítání klientských informací došlo k chybě. Zkuste to prosím později.", + "authorizeErrorInvalidParams": "Parametry požadavku jsou chybné/neplatné. Prosím, zkontrolujte URL a zkuste znovu.", "openidScopeName": "OpenID Connect", - "openidScopeDescription": "Allows the app to access your OpenID Connect information.", - "emailScopeName": "Email", - "emailScopeDescription": "Allows the app to access your email address.", - "profileScopeName": "Profile", - "profileScopeDescription": "Allows the app to access your profile information.", - "groupsScopeName": "Groups", - "groupsScopeDescription": "Allows the app to access your group information.", - "backToLoginButton": "Back to login", - "phoneScopeName": "Phone", - "phoneScopeDescription": "Allows the app to access your phone number.", - "addressScopeName": "Address", - "addressScopeDescription": "Allows the app to access your address.", - "loginTailscaleTitle": "Continue with Tailscale", + "openidScopeDescription": "Povolí aplikaci přístup k Vaším informacím OpenID Connect.", + "emailScopeName": "E-mail", + "emailScopeDescription": "Povolí aplikaci přístup k Vaší e-mailové adrese.", + "profileScopeName": "Profil", + "profileScopeDescription": "Povolí aplikaci přístup k informacím o Vašem profilu.", + "groupsScopeName": "Skupiny", + "groupsScopeDescription": "Povolí aplikaci přístup k informacím o Vaší skupině.", + "backToLoginButton": "Zpět na přihlášení", + "phoneScopeName": "Telefon", + "phoneScopeDescription": "Povolí aplikaci přístup k Vašemu telefonnímu číslu.", + "addressScopeName": "Adresa", + "addressScopeDescription": "Povolí aplikaci přístup k Vaší adrese.", + "loginTailscaleTitle": "Pokračovat přes Tailscale", "loginTailscaleDescription": "You appear to be accessing Tinyauth from an authorized Tailscale device. Would you like to continue with your Tailscale connection?", - "loginTailscaleDeviceName": "Device name:", - "loginTailscaleOtherMethod": "Login with another method", - "loginTailscaleSuccess": "Successfully authenticated with Tailscale.", - "loginTailscaleFail": "Failed to authenticate with Tailscale. Please try again or use another login method.", + "loginTailscaleDeviceName": "Název zařízení:", + "loginTailscaleOtherMethod": "Přihlásit jinou metodou", + "loginTailscaleSuccess": "Úspěšně přihlášeno přes Tailscale.", + "loginTailscaleFail": "Chyba při přihlášení přes Tailscale. Zkuste znovu, případně se přihlašte jiným způsobem, prosím.", "logoutTailscaleSubtitle": "You are currently logged in with Tailscale on your device {{deviceName}}. Click the button below to logout.", - "quickActionsLanguage": "Language", - "quickActionsTheme": "Theme", - "quickActionsThemeLight": "Light", - "quickActionsThemeDark": "Dark", - "quickActionsThemeSystem": "System", - "quickActionsLogout": "Logout", + "quickActionsLanguage": "Jazyk", + "quickActionsTheme": "Motiv", + "quickActionsThemeLight": "Světlý", + "quickActionsThemeDark": "Tmavý", + "quickActionsThemeSystem": "Systémový", + "quickActionsLogout": "Odhlásit se", "quickActionsTitle": "Quick Actions", "quickActionsProviderLocal": "Local", "quickActionsProviderLDAP": "LDAP", diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx index 773cb9f6..730e5f27 100644 --- a/frontend/src/pages/authorize-page.tsx +++ b/frontend/src/pages/authorize-page.tsx @@ -22,7 +22,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { - recompileScreenParams, + searchParamsFromObject, useScreenParams, } from "@/lib/hooks/screen-params"; import { useEffect } from "react"; @@ -89,7 +89,13 @@ export const AuthorizePage = () => { const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); const isOidc = screenParams.login_for === "oidc"; - const compiledParams = recompileScreenParams(screenParams); + const compiledParams = (() => { + const params = searchParamsFromObject(screenParams).toString(); + if (params.length > 0) { + return `?${params}`; + } + return ""; + })(); // TODO: maybe a better way to do this const shouldAutoAuthorize = diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx index a4e34fc5..2e4ee423 100644 --- a/frontend/src/pages/continue-page.tsx +++ b/frontend/src/pages/continue-page.tsx @@ -13,7 +13,7 @@ import { Navigate, useLocation, useNavigate } from "react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRedirectUri } from "@/lib/hooks/redirect-uri"; import { - recompileScreenParams, + searchParamsFromObject, useScreenParams, } from "@/lib/hooks/screen-params"; @@ -31,8 +31,14 @@ export const ContinuePage = () => { const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); const redirectUri = screenParams.redirect_uri; - const isAppLogin = screenParams.login_for === "app"; - const recompiledParams = recompileScreenParams(screenParams); + const isAppLogin = screenParams.login_for === "app" || !screenParams.login_for; + const compiledParams = (() => { + const params = searchParamsFromObject(screenParams).toString(); + if (params.length > 0) { + return `?${params}`; + } + return ""; + })(); const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri( redirectUri, @@ -89,7 +95,7 @@ export const ContinuePage = () => { }, [shouldAutoRedirect, redirectToTarget]); if (!auth.authenticated) { - return ; + return ; } if (!hasValidRedirect || !isAppLogin) { diff --git a/frontend/src/pages/forgot-password-page.tsx b/frontend/src/pages/forgot-password-page.tsx index 58b80a3a..8052701e 100644 --- a/frontend/src/pages/forgot-password-page.tsx +++ b/frontend/src/pages/forgot-password-page.tsx @@ -12,8 +12,8 @@ import { useTranslation } from "react-i18next"; import Markdown from "react-markdown"; import { useLocation } from "react-router"; import { - recompileScreenParams, - useScreenParams, + searchParamsFromObject, + useScreenParams, } from "@/lib/hooks/screen-params"; export const ForgotPasswordPage = () => { @@ -22,7 +22,13 @@ export const ForgotPasswordPage = () => { const { search } = useLocation(); const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); - const compiledParams = recompileScreenParams(screenParams); + const compiledParams = (() => { + const params = searchParamsFromObject(screenParams).toString(); + if (params.length > 0) { + return `?${params}`; + } + return ""; + })(); return ( diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index ab63c72a..91478e6c 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -26,7 +26,7 @@ import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router"; import { toast } from "sonner"; import { - recompileScreenParams, + searchParamsFromObject, useScreenParams, } from "@/lib/hooks/screen-params"; import { useLoginFor } from "@/lib/hooks/login-for"; @@ -63,18 +63,21 @@ export const LoginPage = () => { const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); - const compiledParams = recompileScreenParams({ - ...screenParams, - oidc_prompt: undefined, - }); + const compiledParams = (() => { + const params = searchParamsFromObject(screenParams).toString(); + if (params.length > 0) { + return `?${params}`; + } + return ""; + })(); const loginForUrl = useLoginFor({ login_for: screenParams.login_for, - compiledParams, + params: searchParamsFromObject({ ...screenParams, oidc_prompt: undefined}), }); const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState( providers.find((provider) => provider.id === oauth.autoRedirect) !== - undefined && screenParams.redirect_uri !== undefined, + undefined && (screenParams.redirect_uri || screenParams.oidc_ticket), ); const oauthProviders = providers.filter( @@ -171,8 +174,7 @@ export const LoginPage = () => { !auth.authenticated && isOauthAutoRedirect && !hasAutoRedirectedRef.current && - screenParams.redirect_uri && - screenParams.login_for + (screenParams.redirect_uri || screenParams.oidc_ticket) ) { hasAutoRedirectedRef.current = true; oauthMutate(oauth.autoRedirect); @@ -183,8 +185,8 @@ export const LoginPage = () => { hasAutoRedirectedRef, oauth.autoRedirect, isOauthAutoRedirect, - screenParams.login_for, screenParams.redirect_uri, + screenParams.oidc_ticket ]); useEffect(() => { diff --git a/frontend/src/pages/logout-page.tsx b/frontend/src/pages/logout-page.tsx index d1a734b9..78ef0555 100644 --- a/frontend/src/pages/logout-page.tsx +++ b/frontend/src/pages/logout-page.tsx @@ -17,8 +17,8 @@ import { type UseMutationResult } from "@tanstack/react-query"; import { type AxiosResponse } from "axios"; import { useLocation } from "react-router"; import { + searchParamsFromObject, useScreenParams, - recompileScreenParams, } from "@/lib/hooks/screen-params"; export const LogoutPage = () => { @@ -29,7 +29,13 @@ export const LogoutPage = () => { const redirectTimer = useRef(null); const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); - const compiledParams = recompileScreenParams(screenParams); + const compiledParams = (() => { + const params = searchParamsFromObject(screenParams).toString(); + if (params.length > 0) { + return `?${params}`; + } + return ""; + })(); const logoutMutation = useMutation({ mutationFn: () => axios.post("/api/user/logout"), diff --git a/frontend/src/pages/totp-page.tsx b/frontend/src/pages/totp-page.tsx index e66b4704..3b9a6ef7 100644 --- a/frontend/src/pages/totp-page.tsx +++ b/frontend/src/pages/totp-page.tsx @@ -17,7 +17,7 @@ import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router"; import { toast } from "sonner"; import { - recompileScreenParams, + searchParamsFromObject, useScreenParams, } from "@/lib/hooks/screen-params"; import { useLoginFor } from "@/lib/hooks/login-for"; @@ -32,10 +32,16 @@ export const TotpPage = () => { const searchParams = new URLSearchParams(search); const screenParams = useScreenParams(searchParams); - const compiledParams = recompileScreenParams(screenParams); + const compiledParams = (() => { + const params = searchParamsFromObject(screenParams).toString(); + if (params.length > 0) { + return `?${params}`; + } + return ""; + })(); const loginForUrl = useLoginFor({ login_for: screenParams.login_for, - compiledParams, + params: searchParamsFromObject(screenParams), }); const totpMutation = useMutation({ diff --git a/gen/docs/gen_env.go b/gen/docs/gen_env.go index 041b43cf..02182279 100644 --- a/gen/docs/gen_env.go +++ b/gen/docs/gen_env.go @@ -46,11 +46,14 @@ func generateExampleEnv() { func buildEnvEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]EnvEntry) { desc := child.Tag.Get("description") tag := child.Tag.Get("yaml") + gen := child.Tag.Get("gen") - if tag == "-" { + if tag == "-" && gen != "include" { return } + tag = strings.TrimSuffix(tag, ",omitempty") + value := childValue.Interface() entry := EnvEntry{ diff --git a/gen/docs/gen_md.go b/gen/docs/gen_md.go index a5abf587..60b14d25 100644 --- a/gen/docs/gen_md.go +++ b/gen/docs/gen_md.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "errors" "fmt" "io/fs" @@ -13,9 +12,13 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" ) +type ConfigOptions struct { + Env string + Flag string + YAML string +} type MarkdownEntry struct { - Env string - Flag string + Options ConfigOptions Description string Default any } @@ -47,16 +50,27 @@ func generateMarkdown() { func buildMdEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]MarkdownEntry) { desc := child.Tag.Get("description") tag := child.Tag.Get("yaml") + gen := child.Tag.Get("gen") - if tag == "-" { + if tag == "-" && gen != "include" { return } + tag = strings.TrimSuffix(tag, ",omitempty") + value := childValue.Interface() + configOptions := ConfigOptions{ + Env: strings.ToUpper(strings.ReplaceAll(parentPath, ".", "_")) + strings.ToUpper(child.Name), + Flag: fmt.Sprintf("--%s%s", strings.TrimPrefix(parentPath, "tinyauth."), strings.ToLower(child.Name)), + } + + if tag != "-" && tag != "" { + configOptions.YAML = strings.TrimPrefix(parentPath, "tinyauth.") + tag + } + entry := MarkdownEntry{ - Env: strings.ToUpper(strings.ReplaceAll(parentPath, ".", "_")) + strings.ToUpper(child.Name), - Flag: fmt.Sprintf("--%s%s", strings.TrimPrefix(parentPath, "tinyauth."), strings.ToLower(child.Name)), + Options: configOptions, Description: desc, } @@ -83,11 +97,14 @@ func buildMdMapEntry(child reflect.StructField, parentPath string, entries *[]Ma } tag := child.Tag.Get("yaml") + gen := child.Tag.Get("gen") - if tag == "-" { + if tag == "-" && gen != "include" { return } + tag = strings.TrimSuffix(tag, ",omitempty") + mapPath := parentPath + tag + ".[name]." valueType := fieldType.Elem() @@ -102,27 +119,54 @@ func buildMdChildPath(parent string, child string) string { } func compileMd(entries []MarkdownEntry) []byte { - buffer := bytes.Buffer{} + buffer := strings.Builder{} - buffer.WriteString("\n\n") - buffer.WriteString("# Tinyauth configuration reference\n\n") - buffer.WriteString("| Environment | Flag | Description | Default |\n") - buffer.WriteString("| - | - | - | - |\n") + buffer.WriteString("{/* This part is automatically generated by gen/gen_md.go in the main repository. DO NOT EDIT. */}\n\n") + buffer.WriteString("import { Tabs, TabItem } from '@astrojs/starlight/components';\n\n") + buffer.WriteString("\n") + renderTabItem(&buffer, entries, "Environment") + renderTabItem(&buffer, entries, "Flags") + renderTabItem(&buffer, entries, "YAML") + + buffer.WriteString("\n") + + return []byte(buffer.String()) +} + +func renderTabItem(buffer *strings.Builder, entries []MarkdownEntry, section string) { + buffer.WriteString(fmt.Sprintf(" \n", section)) + buffer.WriteString(" ### main\n\n") + buffer.WriteString(" | Option | Description | Default |\n") + buffer.WriteString(" | - | - | - |\n") + + configType := strings.ToLower(section) previousSection := "" for _, entry := range entries { - if strings.Count(entry.Env, "_") > 1 { - section := strings.Split(strings.TrimPrefix(entry.Env, "TINYAUTH_"), "_")[0] + section := strings.Split(entry.Options.YAML, ".")[0] + var option string + switch configType { + case "yaml": + option = entry.Options.YAML + case "flags": + option = entry.Options.Flag + case "environment": + option = entry.Options.Env + } + if option == "" { + continue + } + if strings.Count(entry.Options.YAML, ".") >= 1 { if section != previousSection { - buffer.WriteString("\n## " + strings.ToLower(section) + "\n\n") - buffer.WriteString("| Environment | Flag | Description | Default |\n") - buffer.WriteString("| - | - | - | - |\n") + buffer.WriteString("\n ### " + strings.ToLower(section) + "\n\n") + buffer.WriteString(" | Option | Description | Default |\n") + buffer.WriteString(" | - | - | - |\n") previousSection = section } } - fmt.Fprintf(&buffer, "| `%s` | `%s` | %s | %s |\n", entry.Env, entry.Flag, entry.Description, entry.Default) + fmt.Fprintf(buffer, " | `%s` | %s | %s |\n", option, entry.Description, entry.Default) } - return buffer.Bytes() + buffer.WriteString(" \n") } diff --git a/go.mod b/go.mod index ca44259f..7f606dcb 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/docker/docker v28.5.2+incompatible github.com/gin-gonic/gin v1.12.0 github.com/go-jose/go-jose/v4 v4.1.4 - github.com/go-ldap/ldap/v3 v3.4.13 + github.com/go-ldap/ldap/v3 v3.4.14 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/go-querystring v1.2.0 @@ -23,13 +23,14 @@ require ( github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 github.com/weppos/publicsuffix-go v0.50.3 go.uber.org/dig v1.19.0 - golang.org/x/crypto v0.53.0 + golang.org/x/crypto v0.54.0 + golang.org/x/net v0.57.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/tools v0.47.0 + golang.org/x/tools v0.48.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/apimachinery v0.36.2 - k8s.io/client-go v0.36.2 - modernc.org/sqlite v1.53.0 + k8s.io/apimachinery v0.36.3 + k8s.io/client-go v0.36.3 + modernc.org/sqlite v1.56.0 ) require ( @@ -72,7 +73,7 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect @@ -91,7 +92,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -134,12 +135,11 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -147,12 +147,12 @@ require ( k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect - modernc.org/libc v1.73.4 // indirect + modernc.org/libc v1.74.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect rsc.io/qr v0.2.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.3 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index f20df9e5..e940ad18 100644 --- a/go.sum +++ b/go.sum @@ -113,12 +113,12 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= -github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= -github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ= +github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= -github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= +github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs= +github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -154,8 +154,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= @@ -208,8 +208,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= @@ -336,29 +336,28 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= @@ -380,32 +379,32 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= -k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= -k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= -k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= -k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= -k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/api v0.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w= +k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg= +k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM= +k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE= +k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg= +k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= -modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= -modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= +modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= -modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= -modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k= +modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -414,8 +413,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= -modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0= +modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= @@ -426,7 +425,7 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.3 h1:u08YRbVUi59ri4YD6cg0UqNM4Dimn0sIl+wldcx5PYw= +sigs.k8s.io/structured-merge-diff/v6 v6.3.3/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 7f05732a..7c8e1ea0 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -88,17 +88,13 @@ func (app *BootstrapApp) Setup() error { app.log.App.Info().Msgf("Starting Tinyauth version: %s", model.Version) // get app url - if app.config.AppURL == "" { - return errors.New("app url cannot be empty, perhaps config loading failed") - } - - appUrl, err := url.Parse(app.config.AppURL) + appURL, err := utils.SafeParseAppURL(app.config.AppURL) if err != nil { return fmt.Errorf("failed to parse app url: %w", err) } - app.runtime.AppURL = strings.ToLower(appUrl.Scheme + "://" + appUrl.Host) + app.runtime.AppURL = appURL // validate session config if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry { @@ -172,7 +168,13 @@ func (app *BootstrapApp) Setup() error { app.runtime.CookieDomain = cookieDomain // cookie names - app.runtime.UUID = utils.GenerateUUID(appUrl.Hostname()) + u, err := url.Parse(app.runtime.AppURL) + + if err != nil { + return fmt.Errorf("failed to parse app url: %w", err) + } + + app.runtime.UUID = utils.GenerateUUID(u.Hostname()) cookieId := strings.Split(app.runtime.UUID, "-")[0] // first 8 characters of the uuid should be good enough diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 495b693d..ae82c2d3 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -30,6 +30,16 @@ func (app *BootstrapApp) setupRouter() error { if err != nil { return fmt.Errorf("failed to set trusted proxies: %w", err) } + + app.runtime.TrustedProxiesConfigured = true + } else { + err := engine.SetTrustedProxies(nil) + + if err != nil { + return fmt.Errorf("failed to set trusted proxies: %w", err) + } + + app.log.App.Warn().Msg("Trusted proxies are not configured, IP access controls will NOT work") } middlewareProvideFor := []any{ diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index 11dbb693..0e61c5fb 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -85,7 +85,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) { err := app.dig.Provide(service.NewKubernetesService) if err != nil { - return nil, fmt.Errorf("failed to provide kubernetes service: %w", err) + app.log.App.Warn().Err(err).Msg("Failed to provide kubernetes service") + return nil, nil } err = app.dig.Invoke(func(k *service.KubernetesService) error { @@ -94,7 +95,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) { }) if err != nil { - return nil, fmt.Errorf("failed to invoke kubernetes service: %w", err) + app.log.App.Warn().Err(err).Msg("Failed to invoke kubernetes service") + return nil, nil } // Kubernetes will fail to initialize with an error if it cannot connect to the cluster @@ -114,7 +116,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) { err := app.dig.Provide(service.NewDockerService) if err != nil { - return nil, fmt.Errorf("failed to provide docker service: %w", err) + app.log.App.Warn().Err(err).Msg("Failed to provide docker service") + return nil, nil } err = app.dig.Invoke(func(d *service.DockerService) error { @@ -123,7 +126,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) { }) if err != nil { - return nil, fmt.Errorf("failed to invoke docker service: %w", err) + app.log.App.Warn().Err(err).Msg("Failed to invoke docker service") + return nil, nil } if app.services.dockerService == nil { diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 27fca206..fd6c2658 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -1,9 +1,9 @@ package controller import ( + "errors" "fmt" "net/http" - "net/url" "strings" "time" @@ -12,6 +12,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "github.com/tinyauthapp/tinyauth/pkg/validators" "go.uber.org/dig" "github.com/gin-gonic/gin" @@ -219,35 +220,16 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - var name string - - if strings.TrimSpace(user.Name) != "" { - controller.log.App.Debug().Msg("Using name from OAuth provider") - name = user.Name - } else { - controller.log.App.Debug().Msg("No name from OAuth provider, generating from email") - parts := strings.SplitN(user.Email, "@", 2) - if len(parts) == 2 { - name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1]) - } else { - name = utils.Capitalize(user.Email) - } - } - - var username string - - if strings.TrimSpace(user.PreferredUsername) != "" { - controller.log.App.Debug().Msg("Using preferred username from OAuth provider") - username = user.PreferredUsername - } else { - controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email") - username = strings.Replace(user.Email, "@", "_", 1) - } + oauthUserInfo := controller.createOAuthUserInfo(oauthUserInfo{ + Username: user.PreferredUsername, + Email: user.Email, + Name: user.Name, + }) sessionCookie := repository.Session{ - Username: username, - Name: name, - Email: user.Email, + Username: oauthUserInfo.Username, + Name: oauthUserInfo.Name, + Email: oauthUserInfo.Email, Provider: svc.ID(), OAuthGroups: utils.CoalesceToString(user.Groups), OAuthName: svc.Name(), @@ -311,56 +293,102 @@ func (controller *OAuthController) getCookieDomain() string { } func (controller *OAuthController) isRedirectSafe(redirectURI string) bool { - u, err := url.Parse(redirectURI) + v := validators.NewDomainValidator(validators.DomainValidatorOptions{ + WithPort: true, + }) + + _, err := v.SafeHostname(controller.runtime.AppURL) if err != nil { - controller.log.App.Error().Err(err).Msg("Failed to parse redirect URI") + controller.log.App.Error().Err(err).Msg("App URL is invalid, cannot validate redirect URI") return false } - if u.Scheme == "" || u.Host == "" { - controller.log.App.Warn().Msg("Redirect URI has invalid scheme or host") - return false - } + err = v.Validate(redirectURI, controller.runtime.AppURL) - au, err := url.Parse(controller.runtime.AppURL) - - if err != nil { - controller.log.App.Error().Err(err).Msg("Failed to parse app URL") - return false - } - - if u.Scheme != au.Scheme { - controller.log.App.Warn().Msg("Redirect URI scheme does not match app URL scheme") - return false - } - - getEffectivePort := func(u *url.URL) string { - if u.Port() != "" { - return u.Port() - } - if u.Scheme == "https" { - return "443" - } - return "80" - } - - if getEffectivePort(u) != getEffectivePort(au) { - controller.log.App.Warn().Msg("Redirect URI port does not match app URL port") - return false - } - - if strings.EqualFold(u.Hostname(), au.Hostname()) { + if err == nil { return true } + controller.log.App.Debug().Err(err).Msg("Failed to validate redirect URI") + + if !errors.Is(err, validators.ErrHostnameMismatch) { + return false + } + if !controller.config.Auth.SubdomainsEnabled { return false } - if strings.HasSuffix(strings.ToLower(u.Hostname()), "."+strings.ToLower(controller.runtime.CookieDomain)) { + v = validators.NewDomainValidator(validators.DomainValidatorOptions{}) + + hostname, err := v.SafeHostname(redirectURI) + + if err != nil { + controller.log.App.Error().Err(err).Msg("Failed to get safe hostname from redirect URI") + return false + } + + if strings.HasSuffix(hostname, "."+strings.ToLower(controller.runtime.CookieDomain)) || + hostname == controller.runtime.CookieDomain { return true } return false } + +type oauthUserInfo struct { + Email string + Username string + Name string +} + +func (controller *OAuthController) createOAuthUserInfo(input oauthUserInfo) oauthUserInfo { + info := oauthUserInfo{ + Email: input.Email, + } + + if controller.config.Experimental.OAuthBridgeEnabled { + if input.Username != "" { + info.Username = input.Username + } else { + parts := strings.SplitN(input.Email, "@", 2) + if len(parts) != 2 { + controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address") + } else { + info.Username = parts[0] + } + } + + if input.Name != "" { + info.Name = input.Name + } else { + info.Name = utils.Capitalize(info.Username) + } + + return info + } + + if input.Name != "" { + controller.log.App.Debug().Msg("Using name from OAuth provider") + info.Name = input.Name + } else { + controller.log.App.Debug().Msg("No name from OAuth provider, generating from email") + parts := strings.SplitN(input.Email, "@", 2) + if len(parts) != 2 { + controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address") + } else { + info.Name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1]) + } + } + + if input.Username != "" { + controller.log.App.Debug().Msg("Using preferred username from OAuth provider") + info.Username = input.Username + } else { + controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email") + info.Username = strings.Replace(info.Email, "@", "_", 1) + } + + return info +} diff --git a/internal/controller/oauth_controller_test.go b/internal/controller/oauth_controller_test.go index 1e3b8aec..72929d42 100644 --- a/internal/controller/oauth_controller_test.go +++ b/internal/controller/oauth_controller_test.go @@ -9,7 +9,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/utils/logger" ) -func TestOAuthControllerIsRedirectSafe(t *testing.T) { +func TestOAuthController_isRedirectSafe(t *testing.T) { log := logger.NewLogger().WithTestConfig() log.Init() @@ -81,22 +81,6 @@ func TestOAuthControllerIsRedirectSafe(t *testing.T) { redirectURI: "https://sub.example.com", expected: false, }, - { - description: "Cookie domain itself is not a subdomain match", - appURL: "https://tinyauth.example.com", - cookieDomain: "example.com", - subdomainsEnabled: true, - redirectURI: "https://example.com", - expected: false, - }, - { - description: "Different scheme returns false", - appURL: "https://tinyauth.example.com", - cookieDomain: "example.com", - subdomainsEnabled: true, - redirectURI: "http://tinyauth.example.com", - expected: false, - }, { description: "Different port returns false", appURL: "https://tinyauth.example.com", @@ -121,14 +105,6 @@ func TestOAuthControllerIsRedirectSafe(t *testing.T) { redirectURI: "https:/malicious", expected: false, }, - { - description: "Redirect URI without scheme returns false", - appURL: "https://tinyauth.example.com", - cookieDomain: "example.com", - subdomainsEnabled: true, - redirectURI: "tinyauth.example.com", - expected: false, - }, { description: "Relative redirect URI returns false", appURL: "https://tinyauth.example.com", diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index e7b14c85..16ac7da8 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "slices" "strconv" "strings" @@ -372,20 +373,14 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) { // Create the authorization code code := controller.oidc.CreateCode(*authorizeReq, *userContext) - queries, err := query.Values(AuthorizeCallback{ - Code: code, - State: authorizeReq.State, - }) + cu, err := url.Parse(authorizeReq.RedirectURI) if err != nil { controller.authorizeError(c, authorizeErrorParams{ - err: err, - reason: "Failed to build query", - reasonPublic: "Failed to build query", - callback: authorizeReq.RedirectURI, - callbackError: "server_error", - state: authorizeReq.State, - json: true, + err: err, + reason: "Failed to parse redirect URI", + reasonPublic: "Failed to parse redirect URI", + json: true, }) return } @@ -406,9 +401,19 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) { http.SetCookie(c.Writer, cookie) + q := cu.Query() + + q.Set("code", code) + + if authorizeReq.State != "" { + q.Set("state", authorizeReq.State) + } + + cu.RawQuery = q.Encode() + c.JSON(200, gin.H{ "status": 200, - "redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()), + "redirect_uri": cu.String(), }) } @@ -684,37 +689,37 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz controller.log.App.Error().Err(params.err).Str("reason", params.reason).Msg("Authorization error") if params.callback != "" { - errorQueries := CallbackError{ - Error: params.callbackError, - } - - if params.reasonPublic != "" { - errorQueries.ErrorDescription = params.reasonPublic - } - - if params.state != "" { - errorQueries.State = params.state - } - - queries, err := query.Values(errorQueries) + cu, err := url.Parse(params.callback) if err != nil { - controller.log.App.Error().Err(err).Msg("Failed to build callback error query") + controller.log.App.Error().Err(err).Msg("Failed to parse callback URL") c.AbortWithStatus(http.StatusInternalServerError) return } - redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode()) + q := cu.Query() + + q.Set("error", params.callbackError) + + if params.reasonPublic != "" { + q.Set("error_description", params.reasonPublic) + } + + if params.state != "" { + q.Set("state", params.state) + } + + cu.RawQuery = q.Encode() if params.json { c.JSON(200, gin.H{ "status": 200, - "redirect_uri": redirectUrl, + "redirect_uri": cu.String(), }) return } - c.Redirect(http.StatusFound, redirectUrl) + c.Redirect(http.StatusFound, cu.String()) return } diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index ffafaffd..32000c2b 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "net/url" + "path" "regexp" "strings" @@ -111,9 +112,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { clientIP := c.ClientIP() aclsCtx := &service.ACLContext{ - ACLs: acls, - IP: net.ParseIP(clientIP), - Path: proxyCtx.Path, + ACLs: acls, + IP: net.ParseIP(clientIP), + Path: proxyCtx.Path, + TrustedProxiesConfigured: controller.runtime.TrustedProxiesConfigured, } if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) { @@ -550,6 +552,19 @@ func (controller *ProxyController) getProxyContext(c *gin.Context) (ProxyContext return ProxyContext{}, err } + // remove any query params from the request path + upath, err := url.Parse(ctx.Path) + + if err != nil { + return ProxyContext{}, fmt.Errorf("failed to parse request path: %v", err) + } + + if upath.Host != "" || !strings.HasPrefix(upath.Path, "/") { + return ProxyContext{}, fmt.Errorf("invalid request path") + } + + ctx.Path = path.Clean(upath.Path) + // We don't care if the header is empty, we will just assume it's not a browser userAgent, _ := controller.getHeader(c, "user-agent") isBrowser := BrowserUserAgentRegex.MatchString(userAgent) diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index faa9934b..1f484fe5 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -308,6 +308,171 @@ func TestProxyController(t *testing.T) { assert.Equal(t, http.StatusOK, recorder.Code) }, }, + { + description: "Ensure path block ACL requires auth on exact match", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/admin") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure path block ACL skips auth for non-matching path", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/public") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusOK, recorder.Code) + }, + }, + { + description: "Ensure path block ACL cannot be bypassed with query params", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/admin?foo=bar") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure path block ACL cannot be bypassed with a trailing slash", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/admin/") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure path block ACL cannot be bypassed with dot segments", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/foo/../admin") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure path block ACL cannot be bypassed with percent-encoded characters", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/%61dmin") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure path block ACL cannot be bypassed with a percent-encoded double slash", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/%2Fadmin") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure a protocol-relative x-forwarded-uri is rejected", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "//admin") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + description: "Ensure a relative x-forwarded-uri is rejected", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "public") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusBadRequest, recorder.Code) + }, + }, + { + description: "Ensure path block ACL cannot be bypassed with query params on envoy ext authz", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/admin%3Ffoo=bar", nil) + req.Host = "path-block.example.com" + req.Header.Set("x-forwarded-proto", "https") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure path block ACL cannot be bypassed with dot segments on nginx auth request", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/nginx", nil) + req.Header.Set("x-original-url", "https://path-block.example.com/foo/../admin") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure path allow ACL still matches when query params are present", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-allow.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed?foo=bar") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusOK, recorder.Code) + }, + }, + { + description: "Ensure path allow ACL cannot be extended with dot segments", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-allow.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed/../secret") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, + { + description: "Ensure an allowed path inside the query string does not bypass auth", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-allow.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/secret?next=/allowed") + router.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusUnauthorized, recorder.Code) + }, + }, { description: "Ensure ip bypass ACL works on forward auth", middlewares: []gin.HandlerFunc{}, @@ -708,7 +873,7 @@ func TestProxyController(t *testing.T) { Log: log, }) - authService := service.NewAuthService(service.AuthServiceInput{ + authService, err := service.NewAuthService(service.AuthServiceInput{ Log: log, Config: &cfg, Runtime: &runtime, @@ -721,6 +886,8 @@ func TestProxyController(t *testing.T) { PolicyEngine: policyEngine, }) + require.NoError(t, err) + for _, test := range tests { t.Run(test.description, func(t *testing.T) { router := gin.Default() diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index ae6c23bf..65b12de0 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -72,26 +72,12 @@ func (controller *UserController) loginHandler(c *gin.Context) { controller.log.App.Debug().Str("username", req.Username).Msg("Login attempt") - isLocked, remaining := controller.auth.IsAccountLocked(req.Username) - - if isLocked { - controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts") - controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "account locked") - c.Writer.Header().Add("x-tinyauth-lock-locked", "true") - c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339)) - c.JSON(429, gin.H{ - "status": 429, - "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining), - }) - return - } - search, err := controller.auth.SearchUser(req.Username) if err != nil { if errors.Is(err, service.ErrUserNotFound) { + controller.auth.DummyPasswordCheck(req.Password) controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt") - controller.auth.RecordLoginAttempt(req.Username, false) controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found") c.JSON(401, gin.H{ "status": 401, @@ -107,14 +93,24 @@ func (controller *UserController) loginHandler(c *gin.Context) { return } + isLocked, remaining := controller.auth.IsAccountLocked(req.Username) + + if isLocked { + controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts") + controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "account locked") + c.Writer.Header().Add("x-tinyauth-lock-locked", "true") + c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339)) + c.JSON(429, gin.H{ + "status": 429, + "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining), + }) + return + } + if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil { controller.log.App.Warn().Str("username", req.Username).Msg("Invalid password during login attempt") controller.auth.RecordLoginAttempt(req.Username, false) - if search.Type == model.UserLocal { - controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "invalid password") - } else { - controller.log.AuditLoginFailure(req.Username, "ldap", c.ClientIP(), "invalid password") - } + controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "invalid password") c.JSON(401, gin.H{ "status": 401, "message": "Unauthorized", @@ -198,6 +194,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { if search.Email != "" { sessionCookie.Email = search.Email } + if search.Name != "" { + sessionCookie.Name = search.Name + } } cookie, err := controller.auth.CreateSession(c, sessionCookie) @@ -215,11 +214,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { controller.log.App.Info().Str("username", req.Username).Msg("Login successful") - if search.Type == model.UserLocal { - controller.log.AuditLoginSuccess(req.Username, "local", c.ClientIP()) - } else { - controller.log.AuditLoginSuccess(req.Username, "ldap", c.ClientIP()) - } + controller.log.AuditLoginSuccess(req.Username, search.Type.String(), c.ClientIP()) controller.auth.RecordLoginAttempt(req.Username, true) diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index 4f081b9b..a971a5d8 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -542,7 +542,8 @@ func TestUserController(t *testing.T) { Runtime: &runtime, Ctx: ctx, }) - authService := service.NewAuthService(service.AuthServiceInput{ + + authService, err := service.NewAuthService(service.AuthServiceInput{ Log: log, Config: &cfg, Runtime: &runtime, @@ -555,6 +556,8 @@ func TestUserController(t *testing.T) { PolicyEngine: policyEngine, }) + require.NoError(t, err) + beforeEach := func() { // Clear failed login attempts before each test authService.ClearLoginAttempts() diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index f49c85a1..3884013d 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -2,6 +2,7 @@ package middleware import ( "context" + "errors" "fmt" "net/http" "strings" @@ -39,6 +40,7 @@ var ( type ContextMiddleware struct { log *logger.Logger runtime *model.RuntimeConfig + config *model.Config auth *service.AuthService broker *service.OAuthBrokerService tailscale *service.TailscaleService @@ -49,6 +51,7 @@ type ContextMiddlewareInput struct { Log *logger.Logger RuntimeConfig *model.RuntimeConfig + StaticConfig *model.Config AuthService *service.AuthService BrokerService *service.OAuthBrokerService TailscaleService *service.TailscaleService @@ -58,6 +61,7 @@ func NewContextMiddleware(i ContextMiddlewareInput) *ContextMiddleware { return &ContextMiddleware{ log: i.Log, runtime: i.RuntimeConfig, + config: i.StaticConfig, auth: i.AuthService, broker: i.BrokerService, tailscale: i.TailscaleService, @@ -200,7 +204,11 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string, ip stri } userContext.LDAP.Groups = user.Groups - userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username) + if search.Name != "" { + userContext.LDAP.Name = search.Name + } else { + userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username) + } userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.runtime.CookieDomain) if search.Email != "" { @@ -244,6 +252,9 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. search, err := m.auth.SearchUser(username) if err != nil { + if errors.Is(err, service.ErrUserNotFound) { + m.auth.DummyPasswordCheck(password) + } return nil, nil, fmt.Errorf("error searching for user: %w", err) } @@ -278,16 +289,20 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. } userContext.Provider = model.ProviderLocal case model.UserLDAP: - user, err := m.auth.GetLDAPUser(username) + user, err := m.auth.GetLDAPUser(search.Username) if err != nil { return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err) } + name := search.Name + if name == "" { + name = utils.Capitalize(username) + } userContext.LDAP = &model.LDAPContext{ BaseContext: model.BaseContext{ Username: username, - Name: utils.Capitalize(username), + Name: name, }, Groups: user.Groups, } @@ -328,16 +343,19 @@ func (m *ContextMiddleware) tailscaleWhois(ip string) (*model.TailscaleContext, return nil, nil } - username := strings.Replace(whois.LoginName, "@", "_", 1) - uctx := model.TailscaleContext{ BaseContext: model.BaseContext{ - Username: username, - Email: whois.LoginName, - Name: whois.DisplayName, + Email: whois.LoginName, + Name: whois.DisplayName, }, NodeName: whois.NodeName, } + if m.config.Experimental.OAuthBridgeEnabled { + uctx.BaseContext.Username = strings.SplitN(whois.LoginName, "@", 2)[0] + } else { + uctx.BaseContext.Username = strings.Replace(whois.LoginName, "@", "_", 1) + } + return &uctx, nil } diff --git a/internal/middleware/context_middleware_test.go b/internal/middleware/context_middleware_test.go index 8a89e419..9a2df892 100644 --- a/internal/middleware/context_middleware_test.go +++ b/internal/middleware/context_middleware_test.go @@ -264,7 +264,8 @@ func TestContextMiddleware(t *testing.T) { Runtime: &runtime, Ctx: ctx, }) - authService := service.NewAuthService(service.AuthServiceInput{ + + authService, err := service.NewAuthService(service.AuthServiceInput{ Log: log, Config: &cfg, Runtime: &runtime, @@ -277,6 +278,8 @@ func TestContextMiddleware(t *testing.T) { PolicyEngine: policyEngine, }) + require.NoError(t, err) + contextMiddleware := NewContextMiddleware(ContextMiddlewareInput{ Log: log, RuntimeConfig: &runtime, diff --git a/internal/model/config.go b/internal/model/config.go index 97c92510..5b077fc5 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -49,7 +49,6 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config { ACLs: ACLsConfig{ Policy: "allow", }, - LockdownEnabled: true, }, UI: UIConfig{ Title: "Tinyauth", @@ -102,22 +101,23 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config { } type Config struct { - AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"` - Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"` - Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"` - Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"` - Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"` - Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"` - Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"` - OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"` - OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"` - UI UIConfig `description:"UI customization." yaml:"ui,omitempty"` - LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"` - Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"` - Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"` - LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"` - Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"` - ConfigFile string `description:"Path to config file." yaml:"-"` + AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"` + ConfigFile string `description:"Path to config file." yaml:"-" gen:"include"` + LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"` + Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"` + Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"` + Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"` + Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"` + Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"` + Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"` + OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"` + OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"` + UI UIConfig `description:"UI customization." yaml:"ui,omitempty"` + LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"` + // enable the cli warning on experimental features + Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"` + Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"` + Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"` } type DatabaseConfig struct { @@ -151,7 +151,6 @@ type AuthConfig struct { SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"` LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"` LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"` - LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled,omitempty"` TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"` ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"` } @@ -239,7 +238,9 @@ type LogStreamConfig struct { Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"` } -type ExperimentalConfig struct{} +type ExperimentalConfig struct { + OAuthBridgeEnabled bool `description:"Enable the OAuth bridge, uses a new way to format OAuth user information." yaml:"oauthBridgeEnabled,omitempty"` +} type TailscaleConfig struct { Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"` @@ -331,6 +332,6 @@ type AppBasicAuth struct { } type AppPath struct { - Allow string `description:"Comma-separated list of allowed paths." yaml:"allow,omitempty"` - Block string `description:"Comma-separated list of blocked paths." yaml:"block,omitempty"` + Allow string `description:"Disable authentication for only paths that match the regex string." yaml:"allow,omitempty"` + Block string `description:"Enable authentication for only paths that match the regex string." yaml:"block,omitempty"` } diff --git a/internal/model/runtime.go b/internal/model/runtime.go index 3731550b..0722e8e1 100644 --- a/internal/model/runtime.go +++ b/internal/model/runtime.go @@ -1,16 +1,17 @@ package model type RuntimeConfig struct { - AppURL string - UUID string - CookieDomain string - SessionCookieName string - ScopeCookieName string - OAuthSessionCookieName string - LocalUsers []LocalUser - OAuthProviders map[string]OAuthServiceConfig - OAuthWhitelist []string - ConfiguredProviders []Provider + AppURL string + UUID string + CookieDomain string + SessionCookieName string + ScopeCookieName string + OAuthSessionCookieName string + LocalUsers []LocalUser + OAuthProviders map[string]OAuthServiceConfig + OAuthWhitelist []string + ConfiguredProviders []Provider + TrustedProxiesConfigured bool } type Provider struct { diff --git a/internal/model/users.go b/internal/model/users.go index 8dc9523d..07420733 100644 --- a/internal/model/users.go +++ b/internal/model/users.go @@ -7,6 +7,16 @@ const ( UserLDAP ) +func (t UserSearchType) String() string { + switch t { + case UserLocal: + return "local" + case UserLDAP: + return "ldap" + } + return "unknown" +} + type LDAPUser struct { DN string Groups []string @@ -22,5 +32,6 @@ type LocalUser struct { type UserSearch struct { Username string Email string // used for LDAP, we can't throw it to LDAPUser because it would need another cache or an LDAP lookup every time + Name string // used for LDAP cn attribute Type UserSearchType } diff --git a/internal/service/access_controls_rules.go b/internal/service/access_controls_rules.go index 98d2f1df..318894d2 100644 --- a/internal/service/access_controls_rules.go +++ b/internal/service/access_controls_rules.go @@ -1,6 +1,7 @@ package service import ( + "errors" "regexp" "strings" @@ -43,6 +44,10 @@ func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect { rule.Log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist") match, err := utils.CheckFilter(ctx.ACLs.OAuth.Whitelist, ctx.UserContext.OAuth.Email) if err != nil { + if errors.Is(err, utils.ErrFilterEmpty) { + rule.Log.App.Debug().Msg("OAuth whitelist is empty, abstaining") + return EffectAbstain + } rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.OAuth.Email).Msg("Invalid entry in OAuth whitelist") return EffectDeny } @@ -72,7 +77,7 @@ func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect { match, err := utils.CheckFilter(ctx.ACLs.Users.Allow, ctx.UserContext.GetUsername()) if err != nil { - if err == utils.ErrFilterEmpty { + if errors.Is(err, utils.ErrFilterEmpty) { return EffectAbstain } rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users allow list") @@ -215,6 +220,10 @@ type IPAllowedRule struct { } func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect { + if !ctx.TrustedProxiesConfigured { + return EffectAllow // We can't block the proxy + } + // merge global and per-app block/allow lists blockedIps := append([]string{}, rule.Config.Auth.IP.Block...) allowedIPs := append([]string{}, rule.Config.Auth.IP.Allow...) @@ -263,6 +272,10 @@ type IPBypassedRule struct { } func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect { + if !ctx.TrustedProxiesConfigured { + return EffectDeny + } + // merge global and per-app bypass lists bypassList := append([]string{}, rule.Config.Auth.IP.Bypass...) if ctx.ACLs != nil { diff --git a/internal/service/access_controls_rules_test.go b/internal/service/access_controls_rules_test.go index 2fc43f51..7cb0f8ba 100644 --- a/internal/service/access_controls_rules_test.go +++ b/internal/service/access_controls_rules_test.go @@ -44,6 +44,21 @@ func TestUserAllowedRule(t *testing.T) { }, expected: EffectAbstain, }, + { + name: "abstains when filter is empty", + ctx: &ACLContext{ + ACLs: &model.App{ + OAuth: model.AppOAuth{Whitelist: ""}, + }, + UserContext: &model.UserContext{ + Provider: model.ProviderOAuth, + OAuth: &model.OAuthContext{ + BaseContext: model.BaseContext{Username: "alice"}, + }, + }, + }, + expected: EffectAbstain, + }, { name: "allows OAuth user when email matches whitelist", ctx: &ACLContext{ @@ -611,11 +626,20 @@ func TestIPAllowedRule(t *testing.T) { ctx *ACLContext expected Effect }{ + { + name: "when trusted proxies are not configured, IP is allowed", + ctx: &ACLContext{ + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + }, + expected: EffectAllow, + }, { name: "allows when ACLs are nil and no global lists configured", ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("10.0.0.1"), + ACLs: nil, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -625,7 +649,8 @@ func TestIPAllowedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Block: []string{"10.0.0.1"}}, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -637,8 +662,9 @@ func TestIPAllowedRule(t *testing.T) { }, }, ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("10.0.0.5"), + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -648,7 +674,8 @@ func TestIPAllowedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Allow: []string{"192.168.1.0/24"}}, }, - IP: net.ParseIP("192.168.1.10"), + IP: net.ParseIP("192.168.1.10"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -660,8 +687,9 @@ func TestIPAllowedRule(t *testing.T) { }, }, ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("192.168.1.10"), + ACLs: &model.App{}, + IP: net.ParseIP("192.168.1.10"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -671,15 +699,17 @@ func TestIPAllowedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Allow: []string{"192.168.1.0/24"}}, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, { name: "allows when no block or allow lists are configured", ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("10.0.0.1"), + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -692,7 +722,8 @@ func TestIPAllowedRule(t *testing.T) { Allow: []string{"10.0.0.1"}, }, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -705,7 +736,8 @@ func TestIPAllowedRule(t *testing.T) { Allow: []string{"10.0.0.1"}, }, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -735,12 +767,23 @@ func TestIPBypassedRule(t *testing.T) { ctx *ACLContext expected Effect }{ + { + name: "when trusted proxies are not configured, IP is not bypassed", + rule: defaultIPBR, + ctx: &ACLContext{ + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: false, + }, + expected: EffectDeny, + }, { name: "deny when ACLs are nil and no global bypass", rule: defaultIPBR, ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("10.0.0.1"), + ACLs: nil, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -748,8 +791,9 @@ func TestIPBypassedRule(t *testing.T) { name: "allows when ACLs are nil but IP matches global bypass", rule: globBypassIPBR, ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("10.0.0.5"), + ACLs: nil, + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -757,8 +801,9 @@ func TestIPBypassedRule(t *testing.T) { name: "denies when ACLs are nil and IP does not match global bypass", rule: globBypassIPBR, ctx: &ACLContext{ - ACLs: nil, - IP: net.ParseIP("192.168.1.1"), + ACLs: nil, + IP: net.ParseIP("192.168.1.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -769,7 +814,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, }, - IP: net.ParseIP("10.0.0.5"), + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -780,7 +826,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"172.16.0.0/24"}}, }, - IP: net.ParseIP("10.0.0.5"), + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -791,7 +838,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, }, - IP: net.ParseIP("10.0.0.5"), + IP: net.ParseIP("10.0.0.5"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, @@ -802,7 +850,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}}, }, - IP: net.ParseIP("192.168.1.1"), + IP: net.ParseIP("192.168.1.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -810,8 +859,9 @@ func TestIPBypassedRule(t *testing.T) { name: "denies when bypass list is empty", rule: defaultIPBR, ctx: &ACLContext{ - ACLs: &model.App{}, - IP: net.ParseIP("10.0.0.1"), + ACLs: &model.App{}, + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectDeny, }, @@ -822,7 +872,8 @@ func TestIPBypassedRule(t *testing.T) { ACLs: &model.App{ IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}}, }, - IP: net.ParseIP("10.0.0.1"), + IP: net.ParseIP("10.0.0.1"), + TrustedProxiesConfigured: true, }, expected: EffectAllow, }, diff --git a/internal/service/access_controls_service.go b/internal/service/access_controls_service.go index 3615cce1..f8816a1f 100644 --- a/internal/service/access_controls_service.go +++ b/internal/service/access_controls_service.go @@ -1,15 +1,17 @@ package service import ( + "errors" "strings" "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "github.com/tinyauthapp/tinyauth/pkg/validators" "go.uber.org/dig" ) type LabelProvider interface { - GetLabels(appDomain string) (*model.App, error) + Lookup(locator func(name string, app *model.App) bool) error } type AccessControlsService struct { @@ -35,27 +37,74 @@ func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsServic } } -func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App { - var nameMatch *model.App +func (service *AccessControlsService) getACLs(domain string, lookup func(locator func(name string, app *model.App) bool) error) (*model.App, error) { + v := validators.NewDomainValidator(validators.DomainValidatorOptions{}) - // First try to find a matching app by domain, then fallback to matching by app name (subdomain) - for app, config := range service.config.Apps { - if config.Config.Domain == domain { - service.log.App.Debug().Str("name", app).Msg("Found matching container by domain") - return &config + var domainMatch *model.App + var nameMatch *model.App + var nameMatchedApps []string + + locatorFunc := func(name string, app *model.App) bool { + if app.Config.Domain != "" { + err := v.Validate(app.Config.Domain, domain) + if err == nil { + service.log.App.Debug().Str("name", name).Msg("Found matching container by domain") + domainMatch = app + return true + } else if !errors.Is(err, validators.ErrHostnameMismatch) { + service.log.App.Debug().Str("name", name).Err(err).Msg("Domain validation failed") + } } - if strings.SplitN(domain, ".", 2)[0] == app { - service.log.App.Debug().Str("name", app).Msg("Found matching container by app name") - nameMatch = &config + if strings.HasPrefix(strings.ToLower(domain), strings.ToLower(name+".")) { + service.log.App.Debug().Str("name", name).Msg("Found matching container by app name") + nameMatch = app + nameMatchedApps = append(nameMatchedApps, name) } + return false } - return nameMatch + err := lookup(locatorFunc) + if err != nil { + return nil, err + } + + if domainMatch != nil { + service.log.App.Debug().Str("domain", domain).Msg("Found matching app by domain") + return domainMatch, nil + } + + if nameMatch == nil { + service.log.App.Debug().Str("domain", domain).Msg("No match found for domain, skipping") + return nil, nil + } + + if len(nameMatchedApps) > 1 { + service.log.App.Warn().Str("domain", domain).Strs("apps", nameMatchedApps).Msg("Multiple apps matched domain by name, app names must be unique, using last match") + } + + service.log.App.Debug().Str("domain", domain).Msg("Found matching app by app name") + return nameMatch, nil +} + +func (service *AccessControlsService) lookupStaticACLs(domain string) (*model.App, error) { + return service.getACLs(domain, func(locator func(name string, app *model.App) bool) error { + for app, config := range service.config.Apps { + if ok := locator(app, &config); ok { + return nil + } + } + return nil + }) } func (service *AccessControlsService) GetAccessControls(domain string) (*model.App, error) { // First check in the static config - app := service.lookupStaticACLs(domain) + app, err := service.lookupStaticACLs(domain) + + // Will never return an error here, but we need to check it + if err != nil { + return nil, err + } if app != nil { service.log.App.Debug().Msg("Using static ACLs for app") @@ -64,9 +113,9 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A // If we have a label provider configured, try to get ACLs from it if service.labelProvider != nil { - return service.labelProvider.GetLabels(domain) + return service.getACLs(domain, service.labelProvider.Lookup) } - // no labels + // No labels return nil, nil } diff --git a/internal/service/access_controls_service_test.go b/internal/service/access_controls_service_test.go index f4f4d24c..30415933 100644 --- a/internal/service/access_controls_service_test.go +++ b/internal/service/access_controls_service_test.go @@ -4,224 +4,184 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" ) -type mockLabelProvider struct { - getLabelsFn func(appDomain string) (*model.App, error) - calledWith string - callCount int +type mockProvider struct { + acls map[string]model.App + shouldError bool } -func (m *mockLabelProvider) GetLabels(appDomain string) (*model.App, error) { - m.calledWith = appDomain - m.callCount++ - if m.getLabelsFn != nil { - return m.getLabelsFn(appDomain) +func newMockProvider(acls map[string]model.App, shouldError bool) *mockProvider { + return &mockProvider{acls: acls, shouldError: shouldError} +} + +func (m *mockProvider) Lookup(locator func(name string, app *model.App) bool) error { + if m.shouldError { + return errors.New("mock error") } - return nil, nil + for name, app := range m.acls { + if ok := locator(name, &app); ok { + return nil + } + } + return nil } -func TestLookupStaticACLs(t *testing.T) { +func TestAccessControlsService(t *testing.T) { log := logger.NewLogger().WithTestConfig() log.Init() tests := []struct { - name string - apps map[string]model.App - domain string - expectNil bool - expectedDomain string + name string + domain string + acls map[string]model.App + want *model.App }{ { - name: "returns nil when no apps are configured", - apps: nil, - domain: "foo.example.com", - expectNil: true, + name: "returns ACLs for domain", + domain: "example.com", + acls: map[string]model.App{ + "foo": {Config: model.AppConfig{Domain: "example.com"}}, + }, + want: &model.App{Config: model.AppConfig{Domain: "example.com"}}, }, { - name: "returns nil when no app matches", - apps: map[string]model.App{ - "foo": {Config: model.AppConfig{Domain: "foo.example.com"}}, + name: "returns ACLs for domain with port", + domain: "example.com:8080", + acls: map[string]model.App{ + "foo": {Config: model.AppConfig{Domain: "example.com"}}, }, - domain: "bar.example.com", - expectNil: true, + want: &model.App{Config: model.AppConfig{Domain: "example.com"}}, }, { - name: "matches by exact domain", - apps: map[string]model.App{ - "foo": {Config: model.AppConfig{Domain: "foo.example.com"}}, + name: "returns ACLs for domain with trailing dot", + domain: "example.com.", + acls: map[string]model.App{ + "foo": {Config: model.AppConfig{Domain: "example.com"}}, }, - domain: "foo.example.com", - expectedDomain: "foo.example.com", + want: &model.App{Config: model.AppConfig{Domain: "example.com"}}, }, { - name: "matches by app name when domain does not match any app", - apps: map[string]model.App{ - "foo": {Config: model.AppConfig{Domain: "configured.example.com"}}, + name: "returns ACLs for non-ascii domain", + domain: "bücher.example.com", + acls: map[string]model.App{ + "foo": {Config: model.AppConfig{Domain: "bücher.example.com"}}, }, - domain: "foo.example.com", - expectedDomain: "configured.example.com", + want: &model.App{Config: model.AppConfig{Domain: "bücher.example.com"}}, }, { - name: "matches by app name for nested subdomains", - apps: map[string]model.App{ - "foo": {Config: model.AppConfig{Domain: "configured.example.com"}}, + name: "returns ACLs for punycode domain and non-ascii config", + domain: "bücher.example.com", + acls: map[string]model.App{ + "foo": {Config: model.AppConfig{Domain: "xn--bcher-kva.example.com"}}, }, - domain: "foo.sub.example.com", - expectedDomain: "configured.example.com", + want: &model.App{Config: model.AppConfig{Domain: "xn--bcher-kva.example.com"}}, }, { - name: "selects the app matching by domain among multiple apps", - apps: map[string]model.App{ - "unrelated": {Config: model.AppConfig{Domain: "other.example.com"}}, - "target": {Config: model.AppConfig{Domain: "foo.example.com"}}, + name: "returns ACLs with case-insensitive matching", + domain: "Example.com", + acls: map[string]model.App{ + "foo": {Config: model.AppConfig{Domain: "example.com"}}, }, - domain: "foo.example.com", - expectedDomain: "foo.example.com", + want: &model.App{Config: model.AppConfig{Domain: "example.com"}}, + }, + { + name: "falls back to name matching when domain fails", + domain: "app.example.com", + acls: map[string]model.App{ + "app": {Path: model.AppPath{Allow: "/foo"}}, + }, + want: &model.App{Path: model.AppPath{Allow: "/foo"}}, + }, + { + name: "name matching is case-insensitive", + domain: "aPp.example.com", + acls: map[string]model.App{ + "APP": {Path: model.AppPath{Allow: "/foo"}}, + }, + want: &model.App{Path: model.AppPath{Allow: "/foo"}}, + }, + { + name: "returns nil when no ACLs are found", + domain: "example.com", + acls: map[string]model.App{}, + want: nil, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := NewAccessControlsService(AccessControlServiceInput{ + // run once for a mock provider + for _, test := range tests { + t.Run(test.name+"(getACLs)", func(t *testing.T) { + mock := newMockProvider(test.acls, false) + acls := NewAccessControlsService(AccessControlServiceInput{ Log: log, - Config: &model.Config{Apps: tt.apps}, - LabelProvider: nil, + Config: &model.Config{}, + LabelProvider: mock, }) - got := svc.lookupStaticACLs(tt.domain) - if tt.expectNil { - assert.Nil(t, got) - return - } - require.NotNil(t, got) - assert.Equal(t, tt.expectedDomain, got.Config.Domain) + app, err := acls.getACLs(test.domain, mock.Lookup) + require.NoError(t, err) + require.Equal(t, test.want, app) }) } -} -func TestGetAccessControls(t *testing.T) { - log := logger.NewLogger().WithTestConfig() - log.Init() - - t.Run("returns static ACLs when domain matches", func(t *testing.T) { - config := model.Config{ - Apps: map[string]model.App{ - "foo": { - Config: model.AppConfig{Domain: "foo.example.com"}, - Users: model.AppUsers{Allow: "alice"}, + // run again for static acls + for _, test := range tests { + t.Run(test.name+"(staticACLs)", func(t *testing.T) { + acls := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{ + Apps: test.acls, }, - }, - } - svc := NewAccessControlsService(AccessControlServiceInput{ - Log: log, - Config: &config, - LabelProvider: nil, + }) + app, err := acls.lookupStaticACLs(test.domain) + require.NoError(t, err) + require.Equal(t, test.want, app) }) + } - got, err := svc.GetAccessControls("foo.example.com") - - require.NoError(t, err) - require.NotNil(t, got) - assert.Equal(t, "foo.example.com", got.Config.Domain) - assert.Equal(t, "alice", got.Users.Allow) + // get acls should return an error when the provider fails + mock := newMockProvider(map[string]model.App{}, true) + acls := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{}, }) + _, err := acls.getACLs("example.com", mock.Lookup) + require.Error(t, err) - t.Run("returns nil when no static match and no label provider", func(t *testing.T) { - svc := NewAccessControlsService(AccessControlServiceInput{ - Log: log, - Config: &model.Config{}, - LabelProvider: nil, - }) - - got, err := svc.GetAccessControls("unknown.example.com") - - require.NoError(t, err) - assert.Nil(t, got) - }) - - t.Run("returns nil when label provider pointer wraps a nil interface", func(t *testing.T) { - var provider LabelProvider - svc := NewAccessControlsService(AccessControlServiceInput{ - Log: log, - Config: &model.Config{}, - LabelProvider: provider, // nil provider - }) - - got, err := svc.GetAccessControls("unknown.example.com") - - require.NoError(t, err) - assert.Nil(t, got) - }) - - t.Run("falls back to label provider when no static match", func(t *testing.T) { - expected := &model.App{ - Config: model.AppConfig{Domain: "dynamic.example.com"}, - Users: model.AppUsers{Allow: "bob"}, - } - mock := &mockLabelProvider{ - getLabelsFn: func(appDomain string) (*model.App, error) { - return expected, nil - }, - } - var provider LabelProvider = mock - svc := NewAccessControlsService(AccessControlServiceInput{ - Log: log, - Config: &model.Config{}, - LabelProvider: provider, - }) - - got, err := svc.GetAccessControls("dynamic.example.com") - - require.NoError(t, err) - assert.Same(t, expected, got) - assert.Equal(t, "dynamic.example.com", mock.calledWith) - assert.Equal(t, 1, mock.callCount) - }) - - t.Run("does not call label provider when static match found", func(t *testing.T) { - mock := &mockLabelProvider{} - var provider LabelProvider = mock - config := model.Config{ + // get access controls should get acls from + // static when static acls are configured + acls = NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{ Apps: map[string]model.App{ "foo": {Config: model.AppConfig{Domain: "foo.example.com"}}, }, - } - svc := NewAccessControlsService(AccessControlServiceInput{ - Log: log, - Config: &config, - LabelProvider: provider, - }) - - got, err := svc.GetAccessControls("foo.example.com") - - require.NoError(t, err) - require.NotNil(t, got) - assert.Equal(t, "foo.example.com", got.Config.Domain) - assert.Equal(t, 0, mock.callCount) + }, }) + app, err := acls.GetAccessControls("foo.example.com") + require.NoError(t, err) + require.Equal(t, &model.App{Config: model.AppConfig{Domain: "foo.example.com"}}, app) - t.Run("propagates label provider errors", func(t *testing.T) { - providerErr := errors.New("provider boom") - mock := &mockLabelProvider{ - getLabelsFn: func(appDomain string) (*model.App, error) { - return nil, providerErr - }, - } - var provider LabelProvider = mock - svc := NewAccessControlsService(AccessControlServiceInput{ - Log: log, - Config: &model.Config{}, - LabelProvider: provider, - }) + // should return nil for no apps + app, err = acls.GetAccessControls("bar.example.com") + require.NoError(t, err) + require.Nil(t, app) - got, err := svc.GetAccessControls("dynamic.example.com") - - assert.Nil(t, got) - assert.ErrorIs(t, err, providerErr) - assert.Equal(t, 1, mock.callCount) + // Should use label provider if available + mock = newMockProvider(map[string]model.App{ + "bar": { + Config: model.AppConfig{Domain: "bar.example.com"}, + }, + }, false) + acls = NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{}, + LabelProvider: mock, }) + app, err = acls.GetAccessControls("bar.example.com") + require.NoError(t, err) + require.Equal(t, &model.App{Config: model.AppConfig{Domain: "bar.example.com"}}, app) } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index eeb5c8e1..0b503e9c 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -8,7 +8,6 @@ import ( "math/big" "net/http" "strings" - "sync" "time" "github.com/steveiliop56/ding" @@ -16,6 +15,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "github.com/tinyauthapp/tinyauth/pkg/cache" "go.uber.org/dig" "github.com/google/uuid" @@ -69,21 +69,13 @@ type AuthService struct { tailscale *TailscaleService policyEngine *PolicyEngine - lockdown struct { - active bool - until time.Time - ctx context.Context - cancelFunc context.CancelFunc - mu sync.RWMutex - } + dummyHash string caches struct { - login *CacheStore[LoginAttempt] - oauth *CacheStore[OAuthPendingSession] - ldap *CacheStore[[]string] + login *cache.CacheStore[LoginAttempt] + oauth *cache.CacheStore[OAuthPendingSession] + ldap *cache.CacheStore[[]string] } - - maxLoginLimits int } type AuthServiceInput struct { @@ -101,7 +93,7 @@ type AuthServiceInput struct { PolicyEngine *PolicyEngine } -func NewAuthService(i AuthServiceInput) *AuthService { +func NewAuthService(i AuthServiceInput) (*AuthService, error) { service := &AuthService{ log: i.Log, runtime: i.Runtime, @@ -114,19 +106,19 @@ func NewAuthService(i AuthServiceInput) *AuthService { policyEngine: i.PolicyEngine, } - // get the max login limits based on the number of users and the configured max retries - service.maxLoginLimits = service.calculateLockdownLimit() + // dummy hash + dummyHash, err := bcrypt.GenerateFromPassword([]byte(utils.GenerateString(8)), bcrypt.DefaultCost) - loginCacheSize := 0 - - if !service.config.Auth.LockdownEnabled { - loginCacheSize = service.maxLoginLimits + if err != nil { + return nil, fmt.Errorf("failed to generate dummy hash: %w", err) } + service.dummyHash = string(dummyHash) + // caches setup - oauthCache := NewCacheStore[OAuthPendingSession](256) - loginCache := NewCacheStore[LoginAttempt](loginCacheSize) - ldapCache := NewCacheStore[[]string](1024) + oauthCache := cache.NewCacheStore[OAuthPendingSession](256) + loginCache := cache.NewCacheStore[LoginAttempt](service.calculateLockdownLimit()) + ldapCache := cache.NewCacheStore[[]string](1024) service.caches.oauth = oauthCache service.caches.login = loginCache @@ -148,7 +140,28 @@ func NewAuthService(i AuthServiceInput) *AuthService { } }, ding.RingMinor) - return service + i.Ding.Go(func(ctx context.Context) { + ticker := time.NewTicker(15 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + service.log.App.Debug().Msg("Updating login cache limits") + service.caches.login.SetMaxSize(service.calculateLockdownLimit()) + service.log.App.Debug().Msg("Login cache limits updated") + case <-ctx.Done(): + return + } + } + + }, ding.RingMinor) + + return service, nil +} + +func (auth *AuthService) DummyPasswordCheck(password string) { + bcrypt.CompareHashAndPassword([]byte(auth.dummyHash), []byte(password)) } func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) { @@ -160,7 +173,7 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) } if auth.ldap != nil { - userDN, email, err := auth.ldap.GetUserInfo(username) + userDN, email, cn, err := auth.ldap.GetUserInfo(username) if err != nil { return nil, fmt.Errorf("failed to get ldap user: %w", err) @@ -169,6 +182,7 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) return &model.UserSearch{ Username: userDN, Email: email, + Name: cn, Type: model.UserLDAP, }, nil } @@ -245,10 +259,6 @@ func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) { } func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { - if locked, remaining := auth.IsInLockdown(); locked { - return true, remaining - } - if auth.config.Auth.LoginMaxRetries <= 0 || auth.config.Auth.LoginTimeout <= 0 { return false, 0 } @@ -271,15 +281,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { return } - if !success && auth.config.Auth.LockdownEnabled && auth.caches.login.Size() >= auth.maxLoginLimits { - if locked, _ := auth.IsInLockdown(); locked { - return - } - go auth.lockdownMode() - return - } - - auth.caches.login.WithLock(func(actions CacheStoreActions[LoginAttempt]) { + auth.caches.login.WithLock(func(actions cache.CacheStoreActions[LoginAttempt]) { entry, ok := actions.Get(identifier) if !ok { @@ -344,7 +346,7 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") } - uuid, err := uuid.NewRandom() + u, err := uuid.NewRandom() if err != nil { return nil, fmt.Errorf("failed to generate session uuid: %w", err) @@ -361,7 +363,7 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess expiresAt := time.Now().Add(time.Duration(expiry) * time.Second) session := repository.CreateSessionParams{ - UUID: uuid.String(), + UUID: u.String(), Username: data.Username, Email: data.Email, Name: data.Name, @@ -616,62 +618,7 @@ func (auth *AuthService) GetOAuthPendingSession(sessionId string) (*OAuthPending return &session, nil } -func (auth *AuthService) lockdownMode() { - auth.lockdown.mu.Lock() - - if auth.lockdown.active { - auth.lockdown.mu.Unlock() - return - } - - ctx, cancel := context.WithCancel(auth.ctx) - - auth.log.App.Warn().Msg("Too many failed login attempts, entering lockdown mode") - - auth.lockdown.active = true - auth.lockdown.ctx = ctx - auth.lockdown.cancelFunc = cancel - - d := time.Duration(auth.config.Auth.LoginTimeout) * time.Second - auth.lockdown.until = time.Now().Add(d) - timer := time.NewTimer(d) - - auth.lockdown.mu.Unlock() - - defer cancel() - defer timer.Stop() - - select { - case <-timer.C: - // Timer expired, end lockdown - case <-ctx.Done(): - // Context cancelled, end lockdown - } - - auth.lockdown.mu.Lock() - - auth.log.App.Info().Msg("Exiting lockdown mode") - - auth.caches.login.Clear() - auth.lockdown.active = false - auth.lockdown.until = time.Time{} - auth.lockdown.ctx = nil - auth.lockdown.cancelFunc = nil - - auth.lockdown.mu.Unlock() -} - -func (auth *AuthService) IsInLockdown() (bool, int) { - auth.lockdown.mu.RLock() - defer auth.lockdown.mu.RUnlock() - if auth.lockdown.active { - remaining := int(time.Until(auth.lockdown.until).Seconds()) - return true, remaining - } - return false, 0 -} - -// mostly a testing function, not useful for anything else +// ClearLoginAttempts is a testing function, not useful for anything else func (auth *AuthService) ClearLoginAttempts() { auth.caches.login.Clear() } diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index 49708b0d..21265a2e 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -2,7 +2,7 @@ package service import ( "context" - "strings" + "fmt" "github.com/steveiliop56/ding" "github.com/tinyauthapp/tinyauth/internal/model" @@ -31,7 +31,6 @@ type DockerServiceInput struct { } func NewDockerService(i DockerServiceInput) (*DockerService, error) { - client, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return nil, err @@ -68,49 +67,38 @@ func (docker *DockerService) inspectContainer(containerId string) (container.Ins return docker.client.ContainerInspect(docker.context, containerId) } -func (docker *DockerService) GetLabels(appDomain string) (*model.App, error) { +func (docker *DockerService) Lookup(locator func(name string, app *model.App) bool) error { if !docker.isConnected { docker.log.App.Debug().Msg("Docker service not connected, returning empty labels") - return nil, nil + return nil } containers, err := docker.getContainers() if err != nil { - return nil, err + return fmt.Errorf("failed to get containers: %w", err) } for _, ctr := range containers { inspect, err := docker.inspectContainer(ctr.ID) if err != nil { - return nil, err + docker.log.App.Error().Err(err).Msgf("Failed to inspect container %s", ctr.ID) + continue } labels, err := decoders.DecodeLabels[model.Apps](inspect.Config.Labels, "apps") if err != nil { - return nil, err + docker.log.App.Warn().Err(err).Msgf("Failed to decode labels for container %s", ctr.ID) + continue } - var nameMatch *model.App - - // First try to find a matching app by domain, then fallback to matching by app name (subdomain) - for appName, appLabels := range labels.Apps { - if appLabels.Config.Domain == appDomain { - docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain") - return &appLabels, nil + for app, config := range labels.Apps { + if ok := locator(app, &config); ok { + return nil } - if strings.SplitN(appDomain, ".", 2)[0] == appName { - docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name") - nameMatch = &appLabels - } - } - - if nameMatch != nil { - return nameMatch, nil } } - docker.log.App.Debug().Str("domain", appDomain).Msg("No matching container found for domain") - return nil, nil + return nil } func (docker *DockerService) watchAndClose(ctx context.Context) { diff --git a/internal/service/kubernetes_service.go b/internal/service/kubernetes_service.go index f065be72..942c5e17 100644 --- a/internal/service/kubernetes_service.go +++ b/internal/service/kubernetes_service.go @@ -12,6 +12,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/decoders" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "github.com/tinyauthapp/tinyauth/pkg/validators" "go.uber.org/dig" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,31 +23,23 @@ import ( "k8s.io/client-go/rest" ) +type ingressEntry struct { + name string + app model.App +} + type ingressKey struct { namespace string name string } -type ingressAppKey struct { - ingressKey - appName string -} - -type ingressApp struct { - domain string - appName string - app model.App -} - type KubernetesService struct { log *logger.Logger - client dynamic.Interface - started bool - mu sync.RWMutex - ingressApps map[ingressKey][]ingressApp - domainIndex map[string]ingressAppKey - appNameIndex map[string]ingressAppKey + client dynamic.Interface + connected bool + mu sync.RWMutex + ingressEntries map[ingressKey][]ingressEntry } type KubernetesServiceInput struct { @@ -86,90 +79,45 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error) i.Log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher") service := &KubernetesService{ - log: i.Log, - client: client, - ingressApps: make(map[ingressKey][]ingressApp), - domainIndex: make(map[string]ingressAppKey), - appNameIndex: make(map[string]ingressAppKey), + log: i.Log, + client: client, + ingressEntries: make(map[ingressKey][]ingressEntry), } i.Ding.Go(func(ctx context.Context) { service.watchGVR(gvr, ctx) }, ding.RingMajor) - service.started = true + service.connected = true i.Log.App.Debug().Msg("Kubernetes label provider started successfully") return service, nil } -func (k *KubernetesService) addIngressApps(namespace, name string, apps []ingressApp) { +func (k *KubernetesService) addIngressEntries(key ingressKey, entries []ingressEntry) { k.mu.Lock() defer k.mu.Unlock() - - key := ingressKey{namespace, name} - // Remove existing entries for this ingress - if existing, ok := k.ingressApps[key]; ok { - for _, app := range existing { - delete(k.domainIndex, app.domain) - delete(k.appNameIndex, app.appName) - } - } - // Add new entries - k.ingressApps[key] = apps - for _, app := range apps { - appKey := ingressAppKey{key, app.appName} - k.domainIndex[app.domain] = appKey - k.appNameIndex[app.appName] = appKey - } + k.ingressEntries[key] = entries } -func (k *KubernetesService) removeIngress(namespace, name string) { +func (k *KubernetesService) removeIngress(key ingressKey) { k.mu.Lock() defer k.mu.Unlock() - - key := ingressKey{namespace, name} - if apps, ok := k.ingressApps[key]; ok { - for _, app := range apps { - delete(k.domainIndex, app.domain) - delete(k.appNameIndex, app.appName) - } - delete(k.ingressApps, key) - } + delete(k.ingressEntries, key) } -func (k *KubernetesService) getByDomain(domain string) *model.App { +func (k *KubernetesService) getEntry(locator func(name string, app *model.App) bool) { k.mu.RLock() defer k.mu.RUnlock() - if appKey, ok := k.domainIndex[domain]; ok { - if apps, ok := k.ingressApps[appKey.ingressKey]; ok { - for i := range apps { - app := &apps[i] - if app.domain == domain && app.appName == appKey.appName { - return &app.app - } + // O(n^2) is not great but the number of ingress entries is expected to be small + for _, entries := range k.ingressEntries { + for _, entry := range entries { + if ok := locator(entry.name, &entry.app); ok { + return } } } - return nil -} - -func (k *KubernetesService) getByAppName(appName string) *model.App { - k.mu.RLock() - defer k.mu.RUnlock() - - if appKey, ok := k.appNameIndex[appName]; ok { - if apps, ok := k.ingressApps[appKey.ingressKey]; ok { - for i := range apps { - app := &apps[i] - if app.appName == appName { - return &app.app - } - } - } - } - return nil } func (k *KubernetesService) extractPaths(rule map[string]any) ([]string, error) { @@ -219,7 +167,8 @@ func (k *KubernetesService) extractHosts(item *unstructured.Unstructured) ([]str } paths, err := k.extractPaths(rule) if err != nil { - // This is purely to warn users, it doesn't affect our ability to extract hosts so we won't fail the whole operation + // This is purely to warn users + // It doesn't affect our ability to extract hosts, so we won't fail the whole operation k.log.App.Warn().Err(err).Str("namespace", item.GetNamespace()).Str("name", item.GetName()).Msg("Failed to extract paths from ingress rule") continue } @@ -235,44 +184,71 @@ func (k *KubernetesService) extractHosts(item *unstructured.Unstructured) ([]str } func (k *KubernetesService) updateFromItem(item *unstructured.Unstructured) { - namespace := item.GetNamespace() - name := item.GetName() + key := ingressKey{ + namespace: item.GetNamespace(), + name: item.GetName(), + } + annotations := item.GetAnnotations() if annotations == nil { - k.removeIngress(namespace, name) + k.removeIngress(key) return } + hosts, err := k.extractHosts(item) if err != nil { - k.removeIngress(namespace, name) + k.removeIngress(key) return } + + if len(hosts) == 0 { + k.log.App.Warn().Str("namespace", key.namespace).Str("name", key.name).Msg("No hosts found in ingress, skipping") + k.removeIngress(key) + return + } + labels, err := decoders.DecodeLabels[model.Apps](annotations, "apps") if err != nil { - k.log.App.Warn().Err(err).Str("namespace", namespace).Str("name", name).Msg("Failed to decode ingress labels, skipping") - k.removeIngress(namespace, name) + k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Msg("Failed to decode ingress labels, skipping") + k.removeIngress(key) return } - var apps []ingressApp - for appName, appLabels := range labels.Apps { - if appLabels.Config.Domain == "" { - continue + + var entries []ingressEntry + + v := validators.NewDomainValidator(validators.DomainValidatorOptions{}) + + for name, config := range labels.Apps { + if config.Config.Domain != "" { + hostname, err := v.SafeHostname(config.Config.Domain) + if err != nil { + k.log.App.Warn().Err(err).Str("namespace", key.namespace).Str("name", key.name).Str("domain", config.Config.Domain).Msg("Domain is invalid, matching will rely on app name") + } else if slices.Contains(hosts, hostname) { + entries = append(entries, ingressEntry{ + name: name, + app: config, + }) + continue + } } - if len(hosts) > 0 && !slices.Contains(hosts, appLabels.Config.Domain) { - k.log.App.Warn().Str("namespace", namespace).Str("name", name).Str("appName", appName).Str("domain", appLabels.Config.Domain).Msg("App domain does not match any hosts defined in ingress rules, skipping") - continue + + for _, host := range hosts { + if strings.HasPrefix(strings.ToLower(host), strings.ToLower(name+".")) { + entries = append(entries, ingressEntry{ + name: name, + app: config, + }) + break + } } - apps = append(apps, ingressApp{ - domain: appLabels.Config.Domain, - appName: appName, - app: appLabels, - }) } - if len(apps) == 0 { - k.removeIngress(namespace, name) - } else { - k.addIngressApps(namespace, name, apps) + + if len(entries) == 0 { + k.removeIngress(key) + return } + + k.addIngressEntries(key, entries) } func (k *KubernetesService) resyncGVR(gvr schema.GroupVersionResource, ctx context.Context) error { @@ -315,7 +291,10 @@ func (k *KubernetesService) runWatcher(gvr schema.GroupVersionResource, w watch. case watch.Added, watch.Modified: k.updateFromItem(item) case watch.Deleted: - k.removeIngress(item.GetNamespace(), item.GetName()) + k.removeIngress(ingressKey{ + namespace: item.GetNamespace(), + name: item.GetName(), + }) } case <-resyncTicker.C: if err := k.resyncGVR(gvr, ctx); err != nil { @@ -362,25 +341,13 @@ func (k *KubernetesService) watchGVR(gvr schema.GroupVersionResource, ctx contex } } -func (k *KubernetesService) GetLabels(appDomain string) (*model.App, error) { - if !k.started { - k.log.App.Debug().Str("domain", appDomain).Msg("Kubernetes label provider not started, skipping") - return nil, nil +func (k *KubernetesService) Lookup(locator func(name string, app *model.App) bool) error { + if !k.connected { + k.log.App.Debug().Msg("Kubernetes label provider not started, skipping") + return nil } - // First check cache - app := k.getByDomain(appDomain) - if app != nil { - k.log.App.Debug().Str("domain", appDomain).Msg("Found labels in cache by domain") - return app, nil - } - appName := strings.SplitN(appDomain, ".", 2)[0] - app = k.getByAppName(appName) - if app != nil { - k.log.App.Debug().Str("domain", appDomain).Str("appName", appName).Msg("Found labels in cache by app name") - return app, nil - } + k.getEntry(locator) - k.log.App.Debug().Str("domain", appDomain).Msg("No labels found for domain") - return nil, nil + return nil } diff --git a/internal/service/kubernetes_service_test.go b/internal/service/kubernetes_service_test.go index 702fe0f8..d1b0b6bc 100644 --- a/internal/service/kubernetes_service_test.go +++ b/internal/service/kubernetes_service_test.go @@ -1,6 +1,7 @@ package service import ( + "strings" "testing" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -25,47 +26,66 @@ func TestKubernetesService(t *testing.T) { description: "Cache by domain returns app and misses unknown domain", run: func(t *testing.T, svc *KubernetesService) { app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}} - svc.addIngressApps("default", "my-ingress", []ingressApp{ - {domain: "foo.example.com", appName: "foo", app: app}, + svc.addIngressEntries(ingressKey{ + namespace: "default", + name: "my-ingress", + }, []ingressEntry{ + { + app: app, + name: "foo", + }, }) - got := svc.getByDomain("foo.example.com") + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "foo.example.com" { + got = app + return true + } + return false + }) require.NotNil(t, got) assert.Equal(t, "foo.example.com", got.Config.Domain) - - got = svc.getByDomain("notfound.example.com") - assert.Nil(t, got) - }, - }, - { - description: "Cache by app name returns app and misses unknown name", - run: func(t *testing.T, svc *KubernetesService) { - app := model.App{Config: model.AppConfig{Domain: "bar.example.com"}} - svc.addIngressApps("default", "my-ingress", []ingressApp{ - {domain: "bar.example.com", appName: "bar", app: app}, - }) - - got := svc.getByAppName("bar") - require.NotNil(t, got) - assert.Equal(t, "bar.example.com", got.Config.Domain) - - got = svc.getByAppName("notfound") - assert.Nil(t, got) }, }, { description: "RemoveIngress clears domain and app name entries", run: func(t *testing.T, svc *KubernetesService) { - app := model.App{Config: model.AppConfig{Domain: "baz.example.com"}} - svc.addIngressApps("default", "my-ingress", []ingressApp{ - {domain: "baz.example.com", appName: "baz", app: app}, + app := model.App{Config: model.AppConfig{Domain: "foo.example.com"}} + svc.addIngressEntries(ingressKey{ + namespace: "default", + name: "my-ingress", + }, []ingressEntry{ + { + app: app, + name: "foo", + }, }) - svc.removeIngress("default", "my-ingress") + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "foo.example.com" { + got = app + return true + } + return false + }) + require.NotNil(t, got) + assert.Equal(t, "foo.example.com", got.Config.Domain) - got := svc.getByDomain("baz.example.com") - assert.Nil(t, got) - got = svc.getByAppName("baz") + got = nil + svc.removeIngress(ingressKey{ + namespace: "default", + name: "my-ingress", + }) + + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "foo.example.com" { + got = app + return true + } + return false + }) assert.Nil(t, got) }, }, @@ -73,67 +93,130 @@ func TestKubernetesService(t *testing.T) { description: "AddIngressApps replaces stale entries for the same ingress", run: func(t *testing.T, svc *KubernetesService) { old := model.App{Config: model.AppConfig{Domain: "old.example.com"}} - svc.addIngressApps("default", "my-ingress", []ingressApp{ - {domain: "old.example.com", appName: "old", app: old}, + svc.addIngressEntries(ingressKey{ + namespace: "default", + name: "my-ingress", + }, []ingressEntry{ + { + app: old, + name: "foo", + }, }) updated := model.App{Config: model.AppConfig{Domain: "new.example.com"}} - svc.addIngressApps("default", "my-ingress", []ingressApp{ - {domain: "new.example.com", appName: "new", app: updated}, + svc.addIngressEntries(ingressKey{ + namespace: "default", + name: "my-ingress", + }, []ingressEntry{ + { + app: updated, + name: "foo", + }, }) - got := svc.getByDomain("old.example.com") + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "old.example.com" { + got = app + return true + } + return false + }) assert.Nil(t, got) - got = svc.getByDomain("new.example.com") + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "new.example.com" { + got = app + return true + } + return false + }) require.NotNil(t, got) assert.Equal(t, "new.example.com", got.Config.Domain) }, }, { - description: "GetLabels returns app from cache when started", + description: "GetLabels returns app from cache when connected", run: func(t *testing.T, svc *KubernetesService) { - svc.started = true + svc.connected = true app := model.App{Config: model.AppConfig{Domain: "hit.example.com"}} - svc.addIngressApps("default", "ing", []ingressApp{ - {domain: "hit.example.com", appName: "hit", app: app}, + svc.addIngressEntries(ingressKey{ + namespace: "default", + name: "my-ingress", + }, []ingressEntry{ + { + app: app, + name: "foo", + }, }) - got, err := svc.GetLabels("hit.example.com") + var got *model.App + err := svc.Lookup(func(name string, app *model.App) bool { + if app.Config.Domain == "hit.example.com" { + got = app + return true + } + return false + }) require.NoError(t, err) + require.NotNil(t, got) assert.Equal(t, "hit.example.com", got.Config.Domain) }, }, { description: "GetLabels returns empty app on cache miss when started", run: func(t *testing.T, svc *KubernetesService) { - svc.started = true + svc.connected = true - got, err := svc.GetLabels("notfound.example.com") + var got *model.App + err := svc.Lookup(func(name string, app *model.App) bool { + if app.Config.Domain == "notfound.example.com" { + got = app + return true + } + return false + }) require.NoError(t, err) - assert.Nil(t, got) + require.Nil(t, got) }, }, { description: "GetLabels resolves app by app name", run: func(t *testing.T, svc *KubernetesService) { - svc.started = true + svc.connected = true - app := model.App{Config: model.AppConfig{Domain: "myapp.internal.example.com"}} - svc.addIngressApps("default", "ing", []ingressApp{ - {domain: "myapp.internal.example.com", appName: "myapp", app: app}, + app := model.App{Path: model.AppPath{Allow: "/foo"}} + svc.addIngressEntries(ingressKey{ + namespace: "default", + name: "my-ingress", + }, []ingressEntry{ + { + app: app, + name: "foo", + }, }) - got, err := svc.GetLabels("myapp.internal.example.com") + var got *model.App + err := svc.Lookup(func(name string, app *model.App) bool { + if strings.HasPrefix("foo.internal.example.com", "foo.") { + got = app + return true + } + return false + }) require.NoError(t, err) - assert.Equal(t, "myapp.internal.example.com", got.Config.Domain) + require.NotNil(t, got) + assert.Equal(t, "/foo", got.Path.Allow) }, }, { description: "GetLabels returns empty app when service not yet started", run: func(t *testing.T, svc *KubernetesService) { - got, err := svc.GetLabels("anything.example.com") + var got *model.App + err := svc.Lookup(func(name string, app *model.App) bool { + return false + }) require.NoError(t, err) assert.Nil(t, got) }, @@ -148,30 +231,437 @@ func TestKubernetesService(t *testing.T) { "tinyauth.apps.myapp.config.domain": "myapp.example.com", "tinyauth.apps.myapp.users.allow": "alice", }) + item.Object["spec"] = map[string]any{ + "rules": []any{ + map[string]any{ + "host": "myapp.example.com", + }, + }, + } svc.updateFromItem(&item) - got := svc.getByDomain("myapp.example.com") + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "myapp.example.com" { + got = app + return true + } + return false + }) + require.NotNil(t, got) assert.Equal(t, "myapp.example.com", got.Config.Domain) assert.Equal(t, "alice", got.Users.Allow) }, }, + { + description: "Update from item skips annotations with no hosts", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.config.domain": "myapp.example.com", + }) + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "myapp.example.com" { + got = app + return true + } + return false + }) + assert.Nil(t, got) + }, + }, + { + description: "UpdateFromItem fails when label parsing fails", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.config.domain": "myapp.example.com", + "tinyauth.apps.myapp.users.break": "i-dont-exist", + }) + item.Object["spec"] = map[string]any{ + "rules": []any{ + map[string]any{ + "host": "myapp.example.com", + }, + }, + } + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "myapp.example.com" { + got = app + return true + } + return false + }) + + require.Nil(t, got) + }, + }, { description: "UpdateFromItem with no annotations removes existing cache entries", run: func(t *testing.T, svc *KubernetesService) { app := model.App{Config: model.AppConfig{Domain: "todelete.example.com"}} - svc.addIngressApps("default", "test-ingress", []ingressApp{ - {domain: "todelete.example.com", appName: "todelete", app: app}, + svc.addIngressEntries(ingressKey{ + namespace: "default", + name: "my-ingress", + }, []ingressEntry{ + { + app: app, + name: "foo", + }, }) item := unstructured.Unstructured{} item.SetNamespace("default") - item.SetName("test-ingress") + item.SetName("my-ingress") svc.updateFromItem(&item) - got := svc.getByDomain("todelete.example.com") + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if app.Config.Domain == "todelete.example.com" { + got = app + return true + } + return false + }) + assert.Nil(t, got) + }, + }, + { + description: "ExtractPaths returns all non empty paths from a rule", + run: func(t *testing.T, svc *KubernetesService) { + rule := map[string]any{ + "http": map[string]any{ + "paths": []any{ + map[string]any{"path": "/"}, + map[string]any{"path": "/api"}, + map[string]any{"path": ""}, + map[string]any{"pathType": "Prefix"}, + "not-a-map", + }, + }, + } + + paths, err := svc.extractPaths(rule) + require.NoError(t, err) + assert.Equal(t, []string{"/", "/api"}, paths) + }, + }, + { + description: "ExtractPaths returns nothing when http or paths are missing", + run: func(t *testing.T, svc *KubernetesService) { + paths, err := svc.extractPaths(map[string]any{}) + require.NoError(t, err) + assert.Empty(t, paths) + + paths, err = svc.extractPaths(map[string]any{ + "http": map[string]any{}, + }) + require.NoError(t, err) + assert.Empty(t, paths) + }, + }, + { + description: "ExtractPaths errors when http is not a map", + run: func(t *testing.T, svc *KubernetesService) { + paths, err := svc.extractPaths(map[string]any{ + "http": "invalid", + }) + require.Error(t, err) + assert.Nil(t, paths) + }, + }, + { + description: "ExtractPaths errors when paths is not a slice", + run: func(t *testing.T, svc *KubernetesService) { + paths, err := svc.extractPaths(map[string]any{ + "http": map[string]any{ + "paths": "invalid", + }, + }) + require.Error(t, err) + assert.Nil(t, paths) + }, + }, + { + description: "ExtractHosts returns hosts from all rules", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{ + map[string]any{ + "host": "foo.example.com", + "http": map[string]any{ + "paths": []any{ + map[string]any{"path": "/"}, + }, + }, + }, + map[string]any{ + "host": "bar.example.com", + }, + map[string]any{ + "host": "", + }, + "not-a-map", + }, "spec", "rules")) + + hosts, err := svc.extractHosts(&item) + require.NoError(t, err) + assert.Equal(t, []string{"foo.example.com", "bar.example.com"}, hosts) + }, + }, + { + description: "ExtractHosts still returns hosts when a rule has no catch all path", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{ + map[string]any{ + "host": "foo.example.com", + "http": map[string]any{ + "paths": []any{ + map[string]any{"path": "/api"}, + }, + }, + }, + }, "spec", "rules")) + + hosts, err := svc.extractHosts(&item) + require.NoError(t, err) + assert.Equal(t, []string{"foo.example.com"}, hosts) + }, + }, + { + description: "ExtractHosts still returns hosts when path extraction fails", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{ + map[string]any{ + "host": "foo.example.com", + "http": "invalid", + }, + }, "spec", "rules")) + + hosts, err := svc.extractHosts(&item) + require.NoError(t, err) + assert.Equal(t, []string{"foo.example.com"}, hosts) + }, + }, + { + description: "ExtractHosts returns nothing when spec.rules is missing", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + + hosts, err := svc.extractHosts(&item) + require.NoError(t, err) + assert.Empty(t, hosts) + }, + }, + { + description: "ExtractHosts errors when spec.rules is not a slice", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules")) + + hosts, err := svc.extractHosts(&item) + require.Error(t, err) + assert.Nil(t, hosts) + }, + }, + { + description: "UpdateFromItem registers app when its domain matches an ingress host", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.config.domain": "myapp.example.com", + }) + require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{ + map[string]any{ + "host": "myapp.example.com", + }, + }, "spec", "rules")) + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if name == "myapp" { + got = app + return true + } + return false + }) + require.NotNil(t, got) + assert.Equal(t, "myapp.example.com", got.Config.Domain) + }, + }, + { + description: "UpdateFromItem registers app when its name matches an ingress host prefix", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.users.allow": "alice", + }) + require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{ + map[string]any{ + "host": "MyApp.example.com", + }, + }, "spec", "rules")) + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if name == "myapp" { + got = app + return true + } + return false + }) + require.NotNil(t, got) + assert.Equal(t, "alice", got.Users.Allow) + }, + }, + { + description: "UpdateFromItem skips apps that match neither host nor name", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.config.domain": "myapp.example.com", + }) + require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{ + map[string]any{ + "host": "other.example.com", + }, + }, "spec", "rules")) + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + got = app + return true + }) + assert.Nil(t, got) + }, + }, + { + description: "UpdateFromItem falls back to app name when the domain is invalid", + run: func(t *testing.T, svc *KubernetesService) { + item := unstructured.Unstructured{} + item.SetNamespace("default") + item.SetName("test-ingress") + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.config.domain": "not a domain", + }) + require.NoError(t, unstructured.SetNestedSlice(item.Object, []any{ + map[string]any{ + "host": "myapp.example.com", + }, + }, "spec", "rules")) + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + if name == "myapp" { + got = app + return true + } + return false + }) + require.NotNil(t, got) + }, + }, + { + description: "UpdateFromItem removes entries when host extraction fails", + run: func(t *testing.T, svc *KubernetesService) { + key := ingressKey{ + namespace: "default", + name: "test-ingress", + } + svc.addIngressEntries(key, []ingressEntry{ + { + app: model.App{Config: model.AppConfig{Domain: "stale.example.com"}}, + name: "foo", + }, + }) + + item := unstructured.Unstructured{} + item.SetNamespace(key.namespace) + item.SetName(key.name) + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.config.domain": "myapp.example.com", + }) + require.NoError(t, unstructured.SetNestedField(item.Object, "invalid", "spec", "rules")) + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + got = app + return true + }) + assert.Nil(t, got) + }, + }, + { + description: "UpdateFromItem removes entries when annotations are not decodable", + run: func(t *testing.T, svc *KubernetesService) { + key := ingressKey{ + namespace: "default", + name: "test-ingress", + } + svc.addIngressEntries(key, []ingressEntry{ + { + app: model.App{Config: model.AppConfig{Domain: "stale.example.com"}}, + name: "foo", + }, + }) + + item := unstructured.Unstructured{} + item.SetNamespace(key.namespace) + item.SetName(key.name) + item.SetAnnotations(map[string]string{ + "tinyauth.apps.myapp.config.oauthWhitelist": "[", + }) + + svc.updateFromItem(&item) + + var got *model.App + svc.getEntry(func(name string, app *model.App) bool { + got = app + return true + }) assert.Nil(t, got) }, }, @@ -180,10 +670,8 @@ func TestKubernetesService(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { svc := &KubernetesService{ - ingressApps: make(map[ingressKey][]ingressApp), - domainIndex: make(map[string]ingressAppKey), - appNameIndex: make(map[string]ingressAppKey), - log: log, + ingressEntries: make(map[ingressKey][]ingressEntry), + log: log, } test.run(t, svc) }) diff --git a/internal/service/ldap_service.go b/internal/service/ldap_service.go index 3c31bab7..2a5a5adb 100644 --- a/internal/service/ldap_service.go +++ b/internal/service/ldap_service.go @@ -146,7 +146,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) { return ldap.conn, nil } -func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, err error) { +func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, cn string, err error) { escapedUsername := ldapgo.EscapeFilter(username) filter := fmt.Sprintf(ldap.config.LDAP.SearchFilter, escapedUsername) @@ -154,7 +154,7 @@ func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, ldap.config.LDAP.BaseDN, ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false, filter, - []string{"dn", "mail"}, + []string{"dn", "mail", "cn"}, nil, ) @@ -163,15 +163,15 @@ func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, searchResult, err := ldap.conn.Search(searchRequest) if err != nil { - return "", "", err + return "", "", "", err } if len(searchResult.Entries) != 1 { - return "", "", fmt.Errorf("multiple or no entries found for user %s", username) + return "", "", "", fmt.Errorf("multiple or no entries found for user %s", username) } entry := searchResult.Entries[0] - return entry.DN, entry.GetAttributeValue("mail"), nil + return entry.DN, entry.GetAttributeValue("mail"), entry.GetAttributeValue("cn"), nil } func (ldap *LdapService) GetUserCount() (int, error) { diff --git a/internal/service/oauth_service.go b/internal/service/oauth_service.go index 44d12555..9667e513 100644 --- a/internal/service/oauth_service.go +++ b/internal/service/oauth_service.go @@ -24,6 +24,7 @@ func NewOAuthService(config model.OAuthServiceConfig, id string, ctx context.Con httpClient := &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ InsecureSkipVerify: config.Insecure, MinVersion: tls.VersionTLS12, diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index b0e8e838..a82ce0a5 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -29,6 +29,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "github.com/tinyauthapp/tinyauth/pkg/cache" "go.uber.org/dig" ) @@ -160,9 +161,9 @@ type OIDCService struct { issuer string caches struct { - code *CacheStore[AuthorizeCodeEntry] - usedCode *CacheStore[UsedCodeEntry] - authorize *CacheStore[AuthorizeRequest] + code *cache.CacheStore[AuthorizeCodeEntry] + usedCode *cache.CacheStore[UsedCodeEntry] + authorize *cache.CacheStore[AuthorizeRequest] } } @@ -347,11 +348,11 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) { i.Ding.Go(service.cleanupRoutine, ding.RingMinor) // Create caches - codeCash := NewCacheStore[AuthorizeCodeEntry](256) - usedCode := NewCacheStore[UsedCodeEntry](256) - authorize := NewCacheStore[AuthorizeRequest](256) + codeCache := cache.NewCacheStore[AuthorizeCodeEntry](256) + usedCode := cache.NewCacheStore[UsedCodeEntry](256) + authorize := cache.NewCacheStore[AuthorizeRequest](256) - service.caches.code = codeCash + service.caches.code = codeCache service.caches.usedCode = usedCode service.caches.authorize = authorize @@ -511,7 +512,7 @@ func (service *OIDCService) GetCodeEntry(codeHash string, clientId string) (*Aut var entry AuthorizeCodeEntry var ok bool - service.caches.code.WithLock(func(actions CacheStoreActions[AuthorizeCodeEntry]) { + service.caches.code.WithLock(func(actions cache.CacheStoreActions[AuthorizeCodeEntry]) { entry, ok = actions.Get(codeHash) if !ok { diff --git a/internal/service/policy_engine.go b/internal/service/policy_engine.go index c3bbb133..943cd4c9 100644 --- a/internal/service/policy_engine.go +++ b/internal/service/policy_engine.go @@ -29,10 +29,11 @@ type Rule interface { } type ACLContext struct { - ACLs *model.App - UserContext *model.UserContext - IP net.IP - Path string + ACLs *model.App + UserContext *model.UserContext + IP net.IP + Path string + TrustedProxiesConfigured bool } type PolicyEngine struct { diff --git a/internal/service/tailscale_service.go b/internal/service/tailscale_service.go index 53cb46b0..68700ac6 100644 --- a/internal/service/tailscale_service.go +++ b/internal/service/tailscale_service.go @@ -10,6 +10,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "github.com/tinyauthapp/tinyauth/pkg/cache" "go.uber.org/dig" ) @@ -59,8 +60,8 @@ type TailscaleService struct { apiToken string caches struct { - devices *CacheStore[tailscaleAPIDevices] - users *CacheStore[tailscaleAPIUsers] + devices *cache.CacheStore[tailscaleAPIDevices] + users *cache.CacheStore[tailscaleAPIUsers] } urls struct { @@ -100,8 +101,8 @@ func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) { apiToken: apiToken, } - devicesCache := NewCacheStore[tailscaleAPIDevices](0) - usersCache := NewCacheStore[tailscaleAPIUsers](0) + devicesCache := cache.NewCacheStore[tailscaleAPIDevices](0) + usersCache := cache.NewCacheStore[tailscaleAPIUsers](0) s.caches.devices = devicesCache s.caches.users = usersCache diff --git a/internal/test/test.go b/internal/test/test.go index a3f07ca0..031d5e1f 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -58,7 +58,15 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) { Domain: "path-allow.example.com", }, Path: model.AppPath{ - Allow: "/allowed", + Allow: "^/allowed$", + }, + }, + "app_path_block": { + Config: model.AppConfig{ + Domain: "path-block.example.com", + }, + Path: model.AppPath{ + Block: "^/admin$", }, }, "app_user_allow": { @@ -163,9 +171,10 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) { }, }, }, - CookieDomain: "example.com", - AppURL: "https://tinyauth.example.com", - SessionCookieName: "tinyauth-session", + CookieDomain: "example.com", + AppURL: "https://tinyauth.example.com", + SessionCookieName: "tinyauth-session", + TrustedProxiesConfigured: true, } return config, runtime diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index 7d30e63a..3bc3546a 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -7,10 +7,55 @@ import ( "strings" "github.com/weppos/publicsuffix-go/publicsuffix" + "golang.org/x/net/idna" ) -// GetCookieDomain parses the app url and returns the domain value to use for cookies. +var ( + ErrEmptyURL = fmt.Errorf("invalid url") +) + +func SafeParseAppURL(str string) (string, error) { + if strings.TrimSpace(str) == "" { + return "", ErrEmptyURL + } + + u, err := url.Parse(str) + + if err != nil { + return "", fmt.Errorf("invalid url: %w", err) + } + + if u.Host == "" || + (u.Scheme != "http" && + u.Scheme != "https") { + return "", fmt.Errorf("invalid url, must be in format https(s)://host") + } + + hostname := strings.ToLower(u.Hostname()) + hostname = strings.TrimSuffix(hostname, ".") + + if netIP := net.ParseIP(hostname); netIP != nil { + return "", fmt.Errorf("ip addresses not allowed") + } + + hostname, err = idna.Lookup.ToASCII(hostname) + + if err != nil { + return "", fmt.Errorf("failed to convert hostname to ascii: %w", err) + } + + appURL := fmt.Sprintf("%s://%s", u.Scheme, hostname) + + if u.Port() != "" { + appURL += ":" + u.Port() + } + + return appURL, nil +} + +// GetCookieDomain parses the app URL and returns the domain value to use for cookies. // When auth for subdomains is enabled, it strips the leftmost label +// GetCookieDomain assumes the app URL is first parsed with SafeParseAppURL // (e.g. sub1.sub2.domain.com -> sub2.domain.com), otherwise it returns the full hostname. func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) { u, err := url.Parse(appUrl) @@ -21,10 +66,6 @@ func GetCookieDomain(appUrl string, subdomainsEnabled bool) (string, error) { hostname := strings.ToLower(u.Hostname()) - if netIP := net.ParseIP(hostname); netIP != nil { - return "", fmt.Errorf("ip addresses not allowed") - } - parts := strings.Split(hostname, ".") if len(parts) < 2 { diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index 296c168a..8c9e9bc5 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -7,7 +7,86 @@ import ( "github.com/tinyauthapp/tinyauth/internal/utils" ) -func TestGetRootDomain(t *testing.T) { +func TestSafeParseAPPURL(t *testing.T) { + // Normal app url + appURL := "http://sub.tinyauth.app" + expected := "http://sub.tinyauth.app" + result, err := utils.SafeParseAppURL(appURL) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // Strip path + appURL = "http://sub.tinyauth.app/path" + expected = "http://sub.tinyauth.app" + result, err = utils.SafeParseAppURL(appURL) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // Preserve port + appURL = "http://sub.tinyauth.app:8080" + expected = "http://sub.tinyauth.app:8080" + result, err = utils.SafeParseAppURL(appURL) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // Remove trailing dot + appURL = "http://sub.tinyauth.app." + expected = "http://sub.tinyauth.app" + result, err = utils.SafeParseAppURL(appURL) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // Convert to ascii + appURL = "http://bücher.example.com" + expected = "http://xn--bcher-kva.example.com" + result, err = utils.SafeParseAppURL(appURL) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // Lowercase + appURL = "HTTP://SUb.tinyAUth.aPP" + expected = "http://sub.tinyauth.app" + result, err = utils.SafeParseAppURL(appURL) + assert.NoError(t, err) + assert.Equal(t, expected, result) + + // Empty string + appURL = "" + _, err = utils.SafeParseAppURL(appURL) + assert.ErrorIs(t, err, utils.ErrEmptyURL) + + // Invalid URL + appURL = "invalidurl" + _, err = utils.SafeParseAppURL(appURL) + assert.ErrorContains(t, err, "invalid url") + + // Non http or https URL + appURL = "ftp://sub.tinyauth.app" + _, err = utils.SafeParseAppURL(appURL) + assert.ErrorContains(t, err, "invalid url") + + // Invalid punycode + appURL = "http://ab--cd.example.com" + _, err = utils.SafeParseAppURL(appURL) + assert.ErrorContains(t, err, "failed to convert hostname to ascii") + + // IP address + appURL = "http://10.10.10.10" + _, err = utils.SafeParseAppURL(appURL) + assert.ErrorContains(t, err, "ip addresses not allowed") + + // IPv6 address + appURL = "http://[::1]:8080" + _, err = utils.SafeParseAppURL(appURL) + assert.ErrorContains(t, err, "ip addresses not allowed") + + // Invalid URL + appURL = "://" + _, err = utils.SafeParseAppURL(appURL) + assert.ErrorContains(t, err, "invalid url") +} + +func TestGetCookieDomain(t *testing.T) { // Normal case domain := "http://sub.tinyauth.app" expected := "tinyauth.app" @@ -27,11 +106,6 @@ func TestGetRootDomain(t *testing.T) { _, err = utils.GetCookieDomain(domain, true) assert.EqualError(t, err, "invalid app url, must be in format subdomain.domain.tld or domain.tld") - // IP address - domain = "http://10.10.10.10" - _, err = utils.GetCookieDomain(domain, true) - assert.ErrorContains(t, err, "ip addresses not allowed") - // Invalid URL domain = "http://[::1]:namedport" _, err = utils.GetCookieDomain(domain, true) @@ -56,6 +130,11 @@ func TestGetRootDomain(t *testing.T) { _, err = utils.GetCookieDomain(domain, true) assert.ErrorContains(t, err, "domain in public suffix list, cannot set cookies") + // Domain managed by ICANN without subdomain + domain = "http://co.uk" + _, err = utils.GetCookieDomain(domain, true) + assert.ErrorContains(t, err, "domain in public suffix list, cannot set cookies") + // Domain without subdomain domain = "http://tinyauth.app" expected = "tinyauth.app" diff --git a/pkg/README.md b/pkg/README.md new file mode 100644 index 00000000..abf6a416 --- /dev/null +++ b/pkg/README.md @@ -0,0 +1,10 @@ +# Public packages + +This directory contains packages that can be used by +other projects. + +While we try to maintain a consistent API, no promises +can be made for non-breaking changes throughout updates +as we constantly need to make changes to comply with the +needs of Tinyauth. We advise pinning the version of the +package you wish to use. \ No newline at end of file diff --git a/internal/service/cache_store.go b/pkg/cache/cache_store.go similarity index 91% rename from internal/service/cache_store.go rename to pkg/cache/cache_store.go index 9dbe057d..a23ca8ec 100644 --- a/internal/service/cache_store.go +++ b/pkg/cache/cache_store.go @@ -1,4 +1,4 @@ -package service +package cache import ( "slices" @@ -33,8 +33,8 @@ func NewCacheStore[T any](maxSize int) *CacheStore[T] { } } -// With lock allows performing multiple operations on the cache store atomically. -// The provided mutate function receives a set of actions (Set, Get, Delete) that +// WithLock allows performing multiple operations on a single lock. +// The provided mutate function receives a set of actions (Set, Get, Delete, Update) that // can be used to manipulate the cache store within the locked context. func (cs *CacheStore[T]) WithLock(mutate func(actions CacheStoreActions[T])) { cs.mu.Lock() @@ -195,3 +195,19 @@ func (cs *CacheStore[T]) Clear() { cs.cache = make(map[string]cacheEntry[T]) cs.order = make([]string, 0) } + +func (cs *CacheStore[T]) SetMaxSize(maxSize int) { + cs.mu.Lock() + defer cs.mu.Unlock() + cs.maxSize = maxSize + for len(cs.cache) > maxSize { + if !cs.evictOne() { + break + } + } +} +func (cs *CacheStore[T]) GetMaxSize() int { + cs.mu.Lock() + defer cs.mu.Unlock() + return cs.maxSize +} diff --git a/internal/service/cache_store_test.go b/pkg/cache/cache_store_test.go similarity index 96% rename from internal/service/cache_store_test.go rename to pkg/cache/cache_store_test.go index 8908017f..71e37bc8 100644 --- a/internal/service/cache_store_test.go +++ b/pkg/cache/cache_store_test.go @@ -1,4 +1,4 @@ -package service +package cache import ( "strconv" @@ -316,6 +316,21 @@ func TestCacheStoreSizeAndClear(t *testing.T) { assert.False(t, ok) } +func TestCacheStoreWithMaxSize(t *testing.T) { + cs := NewCacheStore[string](0) + assert.Equal(t, 0, cs.Size()) + + for i := 0; i < 100; i++ { + cs.Set(strconv.Itoa(i), strconv.Itoa(i), 0) + } + + assert.Equal(t, 100, cs.Size()) + + cs.SetMaxSize(10) + + assert.Equal(t, 10, cs.Size()) +} + func TestCacheStoreWithLock(t *testing.T) { cs := NewCacheStore[int](0) cs.Set("counter", 1, 0) diff --git a/pkg/validators/domain_validator.go b/pkg/validators/domain_validator.go new file mode 100644 index 00000000..d41d82b3 --- /dev/null +++ b/pkg/validators/domain_validator.go @@ -0,0 +1,190 @@ +// Package validators provides validators for various types of data. +// +// Domain validator is a simple utility that ensures two domains are exact +// matches while ensuring that techniques used to bypass such checks do +// not impact the validation. + +package validators + +import ( + "fmt" + "net" + "net/url" + "strings" + + "golang.org/x/net/idna" +) + +// Errors +var ( + ErrSchemeMismatch = fmt.Errorf("scheme mismatch") + ErrPortMismatch = fmt.Errorf("port mismatch") + ErrHostnameMismatch = fmt.Errorf("hostname mismatch") +) + +// DomainValidatorOptions is a set of options for DomainValidator. +type DomainValidatorOptions struct { + // Ensure domains have the same scheme. + WithScheme bool + // Ensure domains have the same port. + WithPort bool + // Specify a list of allowed schemes if WithScheme is set to true. + AllowedSchemes []string +} + +// DomainValidator is a simple utility that ensures two domains are exact +// matches while ensuring that techniques used to bypass such checks do +// not impact the validation. +type DomainValidator struct { + opts DomainValidatorOptions +} + +// NewDomainValidator creates a new DomainValidator. +func NewDomainValidator(opts DomainValidatorOptions) *DomainValidator { + return &DomainValidator{ + opts: opts, + } +} + +func (v *DomainValidator) checkScheme(rawURL string) error { + if !v.opts.WithScheme { + return nil + } + + if len(v.opts.AllowedSchemes) == 0 { + return fmt.Errorf("allowed schemes must be specified") + } + + for _, scheme := range v.opts.AllowedSchemes { + if strings.HasPrefix(strings.ToLower(rawURL), strings.ToLower(scheme)+"://") { + return nil + } + } + + return fmt.Errorf("invalid scheme") + +} + +func (v *DomainValidator) getURL(i string) (*url.URL, error) { + if i == "" { + return nil, fmt.Errorf("url cannot be empty") + } + + if v.opts.WithScheme { + err := v.checkScheme(i) + + if err != nil { + return nil, fmt.Errorf("invalid scheme: %w", err) + } + + u, err := url.Parse(i) + + if err != nil { + return nil, fmt.Errorf("failed to parse input url: %w", err) + } + + if u.Host == "" || u.Scheme == "" { + return nil, fmt.Errorf("missing host or scheme in url: %s", i) + } + + return u, nil + } + + rawURL := i + + if !strings.Contains(i, "://") { + // From godoc: [scheme:][//[userinfo@]host][/]path[?query][#fragment] + // So, we can omit the colon and tell the Go URL lib that we want + // to parse the URL without the scheme. If we don't do this, + // the URL lib will parse our entire domain as the path. + rawURL = "//" + i + } + + u, err := url.Parse(rawURL) + + if err != nil { + return nil, fmt.Errorf("failed to parse host: %w", err) + } + + if u.Host == "" { + return nil, fmt.Errorf("missing host in url: %s", i) + } + + return u, nil +} + +func (v *DomainValidator) getHostname(hostname string) (string, error) { + hostname = strings.ToLower(hostname) + hostname = strings.TrimSuffix(hostname, ".") + if net.ParseIP(hostname) != nil { + return "", fmt.Errorf("ip addresses are not supported") + } + hostname, err := idna.Lookup.ToASCII(hostname) + if err != nil { + return "", fmt.Errorf("failed to convert hostname to ascii: %w", err) + } + return hostname, nil +} + +// Validate ensures that two domains are exact matches with the +// options defined in the DomainValidatorOptions. It ensures that the +// inputs are proper URLs and contain a host. It lowercases the hostnames +// and removes the trailing dot. Finally, it checks that the hostnames are +// equal unless WithScheme or WithPort is set to true where it also +// validates the scheme and port respectively. +func (v *DomainValidator) Validate(expected, actual string) error { + eu, err := v.getURL(expected) + + if err != nil { + return err + } + + au, err := v.getURL(actual) + + if err != nil { + return err + } + + if v.opts.WithScheme { + if eu.Scheme != au.Scheme { + return ErrSchemeMismatch + } + } + + if v.opts.WithPort { + if eu.Port() != au.Port() { + return ErrPortMismatch + } + } + + euf, err := v.getHostname(eu.Hostname()) + + if err != nil { + return err + } + + auf, err := v.getHostname(au.Hostname()) + + if err != nil { + return err + } + + if euf != auf { + return ErrHostnameMismatch + } + + return nil +} + +// SafeHostname uses the internal validation for domains that the validator uses +// to parse a hostname. It ensures the input URL is a valid URL, that a host +// is present and that the hostname is lowercased and without a trailing dot. +func (v *DomainValidator) SafeHostname(input string) (string, error) { + u, err := v.getURL(input) + + if err != nil { + return "", err + } + + return v.getHostname(u.Hostname()) +} diff --git a/pkg/validators/domain_validator_test.go b/pkg/validators/domain_validator_test.go new file mode 100644 index 00000000..aa7587f1 --- /dev/null +++ b/pkg/validators/domain_validator_test.go @@ -0,0 +1,270 @@ +package validators + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDomainValidator_SafeHostname(t *testing.T) { + type testCase struct { + description string + options DomainValidatorOptions + input string + expected string + errorFunc func(t *testing.T, e error) + } + + tests := []testCase{ + { + description: "Empty url fails", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "url cannot be empty") + }, + }, + { + description: "URL without host fails", + input: "/foo", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "missing host in url") + }, + }, + { + description: "Invalid url fails", + input: "foo:foo", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "failed to parse host") + }, + }, + { + description: "With scheme and invalid url should fail", + options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}}, + input: "https://foo:foo", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "failed to parse input url") + }, + }, + { + description: "Scheme disabled with scheme should pass", + input: "https://example.com", + expected: "example.com", + }, + { + description: "Domain without scheme should parse if scheme is disabled", + input: "example.com", + expected: "example.com", + }, + { + description: "Domain without scheme should not parse if scheme is enabled", + options: DomainValidatorOptions{WithScheme: true}, + input: "example.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "invalid scheme") + }, + }, + { + description: "Domain with scheme and disallowed scheme should fail", + options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}}, + input: "foo://example.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "invalid scheme") + }, + }, + { + description: "Domain with scheme and allowed scheme should pass", + options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https"}}, + input: "https://example.com", + expected: "example.com", + }, + { + description: "Domain should get lowercased", + input: "EXAMPLE.COM", + expected: "example.com", + }, + { + description: "DNS dot should be removed", + input: "example.com.", + expected: "example.com", + }, + { + description: "IPv4 address should fail", + input: "127.0.0.1", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "ip addresses are not supported") + }, + }, + { + description: "IPv6 address should fail", + input: "[::1]", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "ip addresses are not supported") + }, + }, + { + description: "Domains with unicode characters should be allowed", + input: "bücher.example.com", + expected: "xn--bcher-kva.example.com", + }, + { + description: "Invalid IDNA domain should fail", + input: "ab--cd.example.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "invalid label") + }, + }, + { + description: "With port enabled without any port should work", + options: DomainValidatorOptions{WithPort: true}, + input: "example.com", + expected: "example.com", + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + v := NewDomainValidator(test.options) + res, err := v.SafeHostname(test.input) + if test.errorFunc != nil { + test.errorFunc(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expected, res) + }) + } +} + +func TestDomainValidator_Validate(t *testing.T) { + type testCase struct { + description string + options DomainValidatorOptions + expected string + actual string + errorFunc func(t *testing.T, e error) + } + + tests := []testCase{ + { + description: "Invalid expected domain fails checks", + expected: "foo:foo", + actual: "bar.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "failed to parse host:") + }, + }, + { + description: "Invalid check domain fails checks", + expected: "example.com", + actual: "foo:foo", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "failed to parse host:") + }, + }, + { + description: "Valid domains with non-matching schemes should fail", + options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}}, + expected: "https://example.com", + actual: "http://example.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorIs(t, e, ErrSchemeMismatch) + }, + }, + { + description: "Valid domains with matching schemes should pass", + options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}}, + expected: "https://example.com", + actual: "https://example.com", + }, + { + description: "Port validation with ports enabled and empty ports should work", + options: DomainValidatorOptions{WithPort: true}, + expected: "example.com", + actual: "example.com", + }, + { + description: "Port validation should pass with same port", + options: DomainValidatorOptions{WithPort: true}, + expected: "example.com:8080", + actual: "example.com:8080", + }, + { + description: "Port enabled with scheme and matching port should pass", + options: DomainValidatorOptions{WithPort: true}, + expected: "https://example.com:443", + actual: "https://example.com:443", + }, + { + description: "Failure to format expected domain should fail", + expected: "ab--cd.example.com", + actual: "example.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "idna: invalid label") + }, + }, + { + description: "Failure to format check domain should fail", + expected: "example.com", + actual: "ab--cd.example.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorContains(t, e, "idna: invalid label") + }, + }, + { + description: "Valid domains with matching schemes and ports should pass", + options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}, WithPort: true}, + expected: "https://example.com:8080", + actual: "https://example.com:8080", + }, + { + description: "Valid domains with matching schemes should pass", + options: DomainValidatorOptions{WithScheme: true, AllowedSchemes: []string{"https", "http"}}, + expected: "https://example.com", + actual: "https://example.com", + }, + { + description: "Valid domains with non matching ports should fail", + options: DomainValidatorOptions{WithPort: true}, + expected: "example.com:8080", + actual: "example.com:8085", + errorFunc: func(t *testing.T, e error) { + assert.ErrorIs(t, e, ErrPortMismatch) + }, + }, + { + description: "Valid domains without ports or schemes should pass", + actual: "example.com", + expected: "example.com", + }, + { + description: "Unicode valid domains should pass", + expected: "xn--bcher-kva.example.com", + actual: "bücher.example.com", + }, + { + description: "Unicode valid domains should pass (reverse)", + expected: "bücher.example.com", + actual: "xn--bcher-kva.example.com", + }, + { + description: "Non matching hostnames should fail", + expected: "example.com", + actual: "foo.com", + errorFunc: func(t *testing.T, e error) { + assert.ErrorIs(t, e, ErrHostnameMismatch) + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + v := NewDomainValidator(test.options) + err := v.Validate(test.expected, test.actual) + if test.errorFunc != nil { + test.errorFunc(t, err) + return + } + require.NoError(t, err) + }) + } +}