From 9c18f960901a0678780f9b251c90badaad108040 Mon Sep 17 00:00:00 2001 From: Sylvain Zimmer Date: Mon, 9 Mar 2026 09:09:34 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(all)=20add=20organizations,=20resourc?= =?UTF-8?q?es,=20channels,=20and=20infra=20migration=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add multi-tenant organization model populated from OIDC claims with org-scoped user discovery, CalDAV principal filtering, and cross-org isolation at the SabreDAV layer. Add bookable resource principals (rooms, equipment) with CalDAV auto-scheduling that handles conflict detection, auto-accept/decline, and org-scoped booking enforcement. Fixes #14. Replace CalendarSubscriptionToken with a unified Channel model supporting CalDAV integration tokens and iCal feed URLs, with encrypted token storage and role-based access control. Fixes #16. Migrate task queue from Celery to Dramatiq with async ICS import, progress tracking, and task status polling endpoint. Replace nginx with Caddy for both the reverse proxy and frontend static serving. Switch frontend package manager from yarn/pnpm to npm and upgrade Node to 24, Next.js to 16, TypeScript to 5.9. Harden security with fail-closed entitlements, RSVP rate limiting and token expiry, CalDAV proxy path validation blocking internal API routes, channel path scope enforcement, and ETag-based conflict prevention. Add frontend pages for resource management and integration channel CRUD, with resource booking in the event modal. Restructure CalDAV paths to /calendars/users/ and /calendars/resources/ with nested principal collections in SabreDAV. --- .github/workflows/calendars-frontend.yml | 12 +- .github/workflows/calendars.yml | 13 +- .github/workflows/crowdin_download.yml | 74 - .github/workflows/crowdin_upload.yml | 68 - .github/workflows/docker-hub.yml | 11 +- .../front-dependencies-installation.yml | 8 +- .gitignore | 3 +- CLAUDE.md | 54 +- Makefile | 373 +- Procfile | 2 +- README.md | 38 +- bin/pylint | 38 - bin/scalingo_postfrontend | 55 +- bin/scalingo_run_web | 11 +- compose.yaml | 61 +- docker/auth/realm.json | 36 +- .../development/etc/nginx/conf.d/default.conf | 45 - docker/files/etc/nginx/conf.d/default.conf | 15 - docker/sabredav/sql/pgsql.principals.sql | 30 - .../src/AutoCreatePrincipalBackend.php | 53 - docker/sabredav/src/ICSImportPlugin.php | 278 - docs/entitlements.md | 17 +- docs/invitations.md | 6 +- docs/organizations.md | 503 + docs/resources.md | 1002 ++ env.d/development/backend.defaults | 17 +- env.d/development/caldav.defaults | 3 +- env.d/development/frontend.defaults | 2 +- src/backend/Dockerfile | 3 - src/backend/calendars/__init__.py | 6 +- src/backend/calendars/celery_app.py | 26 - src/backend/calendars/settings.py | 116 +- src/backend/core/admin.py | 32 +- src/backend/core/api/permissions.py | 74 +- src/backend/core/api/serializers.py | 248 +- src/backend/core/api/viewsets.py | 267 +- src/backend/core/api/viewsets_caldav.py | 161 +- src/backend/core/api/viewsets_channels.py | 148 + src/backend/core/api/viewsets_ical.py | 65 +- src/backend/core/api/viewsets_rsvp.py | 216 +- src/backend/core/api/viewsets_task.py | 104 + src/backend/core/apps.py | 3 +- src/backend/core/authentication/backends.py | 83 +- src/backend/core/entitlements/__init__.py | 2 +- .../core/entitlements/backends/base.py | 6 +- .../entitlements/backends/deploycenter.py | 7 + .../core/entitlements/backends/local.py | 4 +- src/backend/core/enums.py | 9 - src/backend/core/external_api/viewsets.py | 2 +- src/backend/core/factories.py | 42 +- src/backend/core/migrations/0001_initial.py | 70 +- .../0002_calendarsubscriptiontoken.py | 34 - ...endarsubscriptiontoken_token_active_idx.py | 17 - ..._remove_calendarshare_calendar_and_more.py | 19 - src/backend/core/models.py | 366 +- src/backend/core/services/caldav_service.py | 373 +- .../services/calendar_invitation_service.py | 22 +- src/backend/core/services/import_service.py | 38 +- src/backend/core/services/resource_service.py | 182 + .../core/services/translation_service.py | 25 +- src/backend/core/signals.py | 65 +- src/backend/core/task_utils.py | 170 + src/backend/core/tasks.py | 50 + src/backend/core/templates/rsvp/confirm.html | 71 + .../tests/authentication/test_backends.py | 84 +- src/backend/core/tests/conftest.py | 119 +- src/backend/core/tests/test_api_users.py | 66 +- src/backend/core/tests/test_caldav_proxy.py | 85 +- .../core/tests/test_caldav_scheduling.py | 9 +- src/backend/core/tests/test_caldav_service.py | 13 +- .../tests/test_calendar_subscription_api.py | 403 +- src/backend/core/tests/test_channels.py | 456 + src/backend/core/tests/test_cross_org_e2e.py | 709 + src/backend/core/tests/test_entitlements.py | 49 +- src/backend/core/tests/test_ical_export.py | 127 +- src/backend/core/tests/test_import_events.py | 106 +- src/backend/core/tests/test_organizations.py | 196 + .../core/tests/test_permissions_security.py | 382 + src/backend/core/tests/test_resources.py | 309 + src/backend/core/tests/test_rsvp.py | 329 +- src/backend/core/tests/test_signals.py | 205 + src/backend/core/tests/test_tasks.py | 422 + src/backend/core/urls.py | 52 +- src/backend/e2e/viewsets.py | 12 +- .../locale/de_DE/LC_MESSAGES/django.po | 455 - .../locale/en_US/LC_MESSAGES/django.po | 362 - .../locale/fr_FR/LC_MESSAGES/django.po | 371 - .../locale/nl_NL/LC_MESSAGES/django.po | 369 - src/backend/pyproject.toml | 8 +- src/backend/uv.lock | 110 +- src/backend/worker.py | 204 + {docker/sabredav => src/caldav}/Dockerfile | 0 {docker/sabredav => src/caldav}/composer.json | 2 +- .../sabredav => src/caldav}/init-database.sh | 2 +- {docker/sabredav => src/caldav}/php-fpm.conf | 2 +- {docker/sabredav => src/caldav}/sabredav.conf | 0 {docker/sabredav => src/caldav}/server.php | 38 +- .../caldav}/sql/pgsql.addressbooks.sql | 2 +- .../caldav}/sql/pgsql.calendars.sql | 28 +- .../caldav}/sql/pgsql.locks.sql | 4 +- src/caldav/sql/pgsql.principals.sql | 40 + .../caldav}/sql/pgsql.propertystorage.sql | 0 .../caldav}/sql/pgsql.users.sql | 0 .../caldav}/src/ApiKeyAuthBackend.php | 9 +- .../caldav}/src/AttendeeNormalizerPlugin.php | 0 src/caldav/src/AutoCreatePrincipalBackend.php | 197 + .../caldav}/src/CalendarSanitizerPlugin.php | 4 +- src/caldav/src/CalendarsRoot.php | 86 + src/caldav/src/CustomCalDAVPlugin.php | 52 + .../caldav}/src/HttpCallbackIMipPlugin.php | 0 src/caldav/src/InternalApiPlugin.php | 545 + src/caldav/src/PrincipalsRoot.php | 119 + src/caldav/src/ResourceAutoSchedulePlugin.php | 385 + .../src/ResourceMkCalendarBlockPlugin.php | 63 + src/frontend/Caddyfile | 19 + src/frontend/Dockerfile | 33 +- src/frontend/Dockerfile.caddy | 13 + src/frontend/Dockerfile.nginx | 20 - src/frontend/apps/calendars/.env | 2 +- src/frontend/apps/calendars/.env.development | 2 +- src/frontend/apps/calendars/conf/default.conf | 26 - src/frontend/apps/calendars/eslint.config.mjs | 21 +- src/frontend/apps/calendars/jest.config.ts | 8 +- src/frontend/apps/calendars/next.config.ts | 11 +- src/frontend/apps/calendars/package.json | 61 +- .../apps/calendars/src/assets/home/banner.png | Bin 11214 -> 0 bytes .../apps/calendars/src/assets/home/banner.svg | 661 + .../calendars/src/features/api/fetchApi.ts | 37 +- .../apps/calendars/src/features/auth/types.ts | 12 +- .../calendars/src/features/calendar/api.ts | 180 +- .../calendar-list/SubscriptionUrlModal.tsx | 69 +- .../components/calendar-list/utils.ts | 2 +- .../components/scheduler/EventModal.tsx | 44 +- .../event-modal-sections/RemindersSection.tsx | 2 +- .../event-modal-sections/ResourcesSection.tsx | 144 + .../scheduler/hooks/useEventForm.ts | 73 +- .../scheduler/hooks/useSchedulerHandlers.ts | 7 +- .../calendar/components/scheduler/types.ts | 6 +- .../features/calendar/hooks/useCalendars.ts | 81 +- .../calendar/services/dav/CalDavService.ts | 13 +- .../services/dav/EventCalendarAdapter.ts | 3 + .../src/features/calendar/utils/DavClient.ts | 2 +- .../src/features/i18n/translations.json | 258 +- .../features/integrations/api/useChannels.ts | 83 + .../integrations/components/ChannelCard.tsx | 68 + .../integrations/components/ChannelList.tsx | 156 + .../components/CreateChannelModal.tsx | 153 + .../components/DeleteChannelModal.tsx | 74 + .../integrations/components/Integrations.scss | 154 + .../components/TokenRevealBox.tsx | 49 + .../src/features/integrations/types.ts | 29 + .../layouts/components/header/Header.tsx | 61 +- .../layouts/components/header/index.scss | 7 + .../resources/api/useResourcePrincipals.ts | 104 + .../features/resources/api/useResources.ts | 43 + .../components/CreateResourceModal.tsx | 121 + .../components/DeleteResourceModal.tsx | 73 + .../resources/components/ResourceCard.tsx | 53 + .../resources/components/ResourceList.tsx | 173 + .../resources/components/Resources.scss | 131 + .../calendars/src/features/resources/types.ts | 15 + .../components/logo/DynamicCalendarLogo.scss | 2 +- .../apps/calendars/src/pages/calendar.tsx | 32 +- .../apps/calendars/src/pages/index.tsx | 26 +- .../apps/calendars/src/pages/integrations.tsx | 74 + .../apps/calendars/src/pages/no-access.tsx | 12 +- .../apps/calendars/src/pages/resources.tsx | 72 + .../apps/calendars/src/styles/globals.scss | 1 + src/frontend/apps/calendars/tsconfig.json | 25 +- src/frontend/apps/e2e/playwright.config.ts | 2 +- .../docker/files/usr/local/bin/entrypoint | 4 - src/frontend/package-lock.json | 14344 ++++++++++++++++ src/frontend/package.json | 26 +- src/frontend/yarn.lock | 7298 -------- src/nginx/servers.conf.erb | 97 - src/proxy/Caddyfile | 95 + 176 files changed, 26903 insertions(+), 12108 deletions(-) delete mode 100644 .github/workflows/crowdin_download.yml delete mode 100644 .github/workflows/crowdin_upload.yml delete mode 100755 bin/pylint delete mode 100644 docker/files/development/etc/nginx/conf.d/default.conf delete mode 100644 docker/files/etc/nginx/conf.d/default.conf delete mode 100644 docker/sabredav/sql/pgsql.principals.sql delete mode 100644 docker/sabredav/src/AutoCreatePrincipalBackend.php delete mode 100644 docker/sabredav/src/ICSImportPlugin.php create mode 100644 docs/organizations.md create mode 100644 docs/resources.md delete mode 100644 src/backend/calendars/celery_app.py create mode 100644 src/backend/core/api/viewsets_channels.py create mode 100644 src/backend/core/api/viewsets_task.py delete mode 100644 src/backend/core/migrations/0002_calendarsubscriptiontoken.py delete mode 100644 src/backend/core/migrations/0003_calendarsubscriptiontoken_token_active_idx.py delete mode 100644 src/backend/core/migrations/0004_remove_calendarshare_calendar_and_more.py create mode 100644 src/backend/core/services/resource_service.py create mode 100644 src/backend/core/task_utils.py create mode 100644 src/backend/core/tasks.py create mode 100644 src/backend/core/templates/rsvp/confirm.html create mode 100644 src/backend/core/tests/test_channels.py create mode 100644 src/backend/core/tests/test_cross_org_e2e.py create mode 100644 src/backend/core/tests/test_organizations.py create mode 100644 src/backend/core/tests/test_permissions_security.py create mode 100644 src/backend/core/tests/test_resources.py create mode 100644 src/backend/core/tests/test_signals.py create mode 100644 src/backend/core/tests/test_tasks.py delete mode 100644 src/backend/locale/de_DE/LC_MESSAGES/django.po delete mode 100644 src/backend/locale/en_US/LC_MESSAGES/django.po delete mode 100644 src/backend/locale/fr_FR/LC_MESSAGES/django.po delete mode 100644 src/backend/locale/nl_NL/LC_MESSAGES/django.po create mode 100644 src/backend/worker.py rename {docker/sabredav => src/caldav}/Dockerfile (100%) rename {docker/sabredav => src/caldav}/composer.json (85%) rename {docker/sabredav => src/caldav}/init-database.sh (97%) rename {docker/sabredav => src/caldav}/php-fpm.conf (96%) rename {docker/sabredav => src/caldav}/sabredav.conf (100%) rename {docker/sabredav => src/caldav}/server.php (80%) rename {docker/sabredav => src/caldav}/sql/pgsql.addressbooks.sql (97%) rename {docker/sabredav => src/caldav}/sql/pgsql.calendars.sql (85%) rename {docker/sabredav => src/caldav}/sql/pgsql.locks.sql (89%) create mode 100644 src/caldav/sql/pgsql.principals.sql rename {docker/sabredav => src/caldav}/sql/pgsql.propertystorage.sql (100%) rename {docker/sabredav => src/caldav}/sql/pgsql.users.sql (100%) rename {docker/sabredav => src/caldav}/src/ApiKeyAuthBackend.php (87%) rename {docker/sabredav => src/caldav}/src/AttendeeNormalizerPlugin.php (100%) create mode 100644 src/caldav/src/AutoCreatePrincipalBackend.php rename {docker/sabredav => src/caldav}/src/CalendarSanitizerPlugin.php (98%) create mode 100644 src/caldav/src/CalendarsRoot.php create mode 100644 src/caldav/src/CustomCalDAVPlugin.php rename {docker/sabredav => src/caldav}/src/HttpCallbackIMipPlugin.php (100%) create mode 100644 src/caldav/src/InternalApiPlugin.php create mode 100644 src/caldav/src/PrincipalsRoot.php create mode 100644 src/caldav/src/ResourceAutoSchedulePlugin.php create mode 100644 src/caldav/src/ResourceMkCalendarBlockPlugin.php create mode 100644 src/frontend/Caddyfile create mode 100644 src/frontend/Dockerfile.caddy delete mode 100644 src/frontend/Dockerfile.nginx delete mode 100644 src/frontend/apps/calendars/conf/default.conf delete mode 100644 src/frontend/apps/calendars/src/assets/home/banner.png create mode 100644 src/frontend/apps/calendars/src/assets/home/banner.svg create mode 100644 src/frontend/apps/calendars/src/features/calendar/components/scheduler/event-modal-sections/ResourcesSection.tsx create mode 100644 src/frontend/apps/calendars/src/features/integrations/api/useChannels.ts create mode 100644 src/frontend/apps/calendars/src/features/integrations/components/ChannelCard.tsx create mode 100644 src/frontend/apps/calendars/src/features/integrations/components/ChannelList.tsx create mode 100644 src/frontend/apps/calendars/src/features/integrations/components/CreateChannelModal.tsx create mode 100644 src/frontend/apps/calendars/src/features/integrations/components/DeleteChannelModal.tsx create mode 100644 src/frontend/apps/calendars/src/features/integrations/components/Integrations.scss create mode 100644 src/frontend/apps/calendars/src/features/integrations/components/TokenRevealBox.tsx create mode 100644 src/frontend/apps/calendars/src/features/integrations/types.ts create mode 100644 src/frontend/apps/calendars/src/features/resources/api/useResourcePrincipals.ts create mode 100644 src/frontend/apps/calendars/src/features/resources/api/useResources.ts create mode 100644 src/frontend/apps/calendars/src/features/resources/components/CreateResourceModal.tsx create mode 100644 src/frontend/apps/calendars/src/features/resources/components/DeleteResourceModal.tsx create mode 100644 src/frontend/apps/calendars/src/features/resources/components/ResourceCard.tsx create mode 100644 src/frontend/apps/calendars/src/features/resources/components/ResourceList.tsx create mode 100644 src/frontend/apps/calendars/src/features/resources/components/Resources.scss create mode 100644 src/frontend/apps/calendars/src/features/resources/types.ts create mode 100644 src/frontend/apps/calendars/src/pages/integrations.tsx create mode 100644 src/frontend/apps/calendars/src/pages/resources.tsx delete mode 100755 src/frontend/docker/files/usr/local/bin/entrypoint create mode 100644 src/frontend/package-lock.json delete mode 100644 src/frontend/yarn.lock delete mode 100644 src/nginx/servers.conf.erb create mode 100644 src/proxy/Caddyfile diff --git a/.github/workflows/calendars-frontend.yml b/.github/workflows/calendars-frontend.yml index 8ed69cc..cdba22d 100644 --- a/.github/workflows/calendars-frontend.yml +++ b/.github/workflows/calendars-frontend.yml @@ -24,16 +24,16 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "22.x" + node-version: "24.x" - name: Restore the frontend cache uses: actions/cache@v5 with: path: "src/frontend/**/node_modules" - key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }} + key: front-node_modules-${{ hashFiles('src/frontend/**/package-lock.json') }} fail-on-cache-miss: true - name: Check linting - run: cd src/frontend/ && yarn lint + run: cd src/frontend/ && npm run lint test-unit: runs-on: ubuntu-latest @@ -45,16 +45,16 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "22.x" + node-version: "24.x" - name: Restore the frontend cache uses: actions/cache@v5 with: path: "src/frontend/**/node_modules" - key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }} + key: front-node_modules-${{ hashFiles('src/frontend/**/package-lock.json') }} fail-on-cache-miss: true - name: Run unit tests run: | cd src/frontend/apps/calendars - TZ=Europe/Paris yarn test + TZ=Europe/Paris npm test diff --git a/.github/workflows/calendars.yml b/.github/workflows/calendars.yml index 1ab02ef..17d4cfb 100644 --- a/.github/workflows/calendars.yml +++ b/.github/workflows/calendars.yml @@ -65,6 +65,7 @@ jobs: CALDAV_URL: http://localhost:80 CALDAV_OUTBOUND_API_KEY: test-outbound-key CALDAV_INBOUND_API_KEY: test-inbound-key + CALDAV_INTERNAL_API_KEY: test-internal-key CALDAV_CALLBACK_HOST: localhost TRANSLATIONS_JSON_PATH: ${{ github.workspace }}/src/frontend/apps/calendars/src/features/i18n/translations.json @@ -80,7 +81,7 @@ jobs: - name: Build and start CalDAV server working-directory: . run: | - docker build -t caldav-test docker/sabredav + docker build -t caldav-test src/caldav docker run -d --name caldav-test \ --network host \ -e PGHOST=localhost \ @@ -88,9 +89,10 @@ jobs: -e PGDATABASE=calendars \ -e PGUSER=pgroot \ -e PGPASSWORD=pass \ - -e CALDAV_BASE_URI=/api/v1.0/caldav/ \ + -e CALDAV_BASE_URI=/caldav/ \ -e CALDAV_INBOUND_API_KEY=test-inbound-key \ -e CALDAV_OUTBOUND_API_KEY=test-outbound-key \ + -e CALDAV_INTERNAL_API_KEY=test-internal-key \ caldav-test \ sh -c "/usr/local/bin/init-database.sh && apache2-foreground" @@ -108,13 +110,10 @@ jobs: - name: Install the dependencies run: uv sync --locked --all-extras - - name: Install gettext (required to compile messages) and MIME support + - name: Install MIME support run: | sudo apt-get update - sudo apt-get install -y gettext pandoc shared-mime-info media-types - - - name: Generate a MO file from strings extracted from the project - run: uv run python manage.py compilemessages + sudo apt-get install -y pandoc shared-mime-info media-types - name: Run tests run: uv run pytest -n 2 diff --git a/.github/workflows/crowdin_download.yml b/.github/workflows/crowdin_download.yml deleted file mode 100644 index a89ef78..0000000 --- a/.github/workflows/crowdin_download.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Download translations from Crowdin - -on: - workflow_dispatch: - push: - branches: - - 'release/**' - -jobs: - install-front: - uses: ./.github/workflows/front-dependencies-installation.yml - with: - node_version: '22.x' - - synchronize-with-crowdin: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Create empty source files - run: | - touch src/backend/locale/django.pot - # crowdin workflow - - name: crowdin action - uses: crowdin/github-action@v2 - with: - config: crowdin/config.yml - upload_sources: false - upload_translations: false - download_translations: true - create_pull_request: false - push_translations: false - push_sources: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # A numeric ID, found at https://crowdin.com/project//tools/api - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - - # Visit https://crowdin.com/settings#api-key to create this token - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - CROWDIN_BASE_PATH: "../src/" - # frontend i18n - - name: Restore the frontend cache - uses: actions/cache@v5 - with: - path: "src/frontend/**/node_modules" - key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }} - fail-on-cache-miss: true - - name: generate translations files - working-directory: src/frontend - run: yarn i18n:deploy - # Create a new PR - - name: Create a new Pull Request with new translated strings - uses: peter-evans/create-pull-request@v7 - with: - commit-message: | - 🌐(i18n) update translated strings - - Update translated files with new translations - title: 🌐(i18n) update translated strings - body: | - ## Purpose - - update translated strings - - ## Proposal - - - [x] update translated strings - branch: i18n/update-translations - labels: i18n diff --git a/.github/workflows/crowdin_upload.yml b/.github/workflows/crowdin_upload.yml deleted file mode 100644 index 87516d9..0000000 --- a/.github/workflows/crowdin_upload.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Update crowdin sources - -on: - workflow_dispatch: - push: - branches: - - main - -jobs: - install-front: - uses: ./.github/workflows/front-dependencies-installation.yml - with: - node_version: '22.x' - - synchronize-with-crowdin: - needs: install-front - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - # Backend i18n - - name: Install Python - uses: actions/setup-python@v6 - with: - python-version: "3.13.9" - cache: 'pip' - - name: Upgrade pip and setuptools - run: pip install --upgrade pip setuptools - - name: Install development dependencies - run: pip install --user . - working-directory: src/backend - - name: Install gettext - run: | - sudo apt-get update - sudo apt-get install -y gettext pandoc - - name: generate pot files - working-directory: src/backend - run: | - DJANGO_CONFIGURATION=Build python manage.py makemessages -a --keep-pot - # frontend i18n - - name: Restore the frontend cache - uses: actions/cache@v5 - with: - path: "src/frontend/**/node_modules" - key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }} - fail-on-cache-miss: true - - name: generate source translation file - working-directory: src/frontend - run: yarn i18n:extract - # crowdin workflow - - name: crowdin action - uses: crowdin/github-action@v2 - with: - config: crowdin/config.yml - upload_sources: true - upload_translations: false - download_translations: false - create_pull_request: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # A numeric ID, found at https://crowdin.com/project//tools/api - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - - # Visit https://crowdin.com/settings#api-key to create this token - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - CROWDIN_BASE_PATH: "../src/" diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index 7dba459..04fe306 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -49,7 +49,7 @@ jobs: context: ./src/backend target: backend-production platforms: ${{ github.event_name != 'pull_request' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} - build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000 + build-args: DOCKER_USER=${{ env.DOCKER_USER }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -81,7 +81,7 @@ jobs: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - name: Build SSG assets (platform-independent, amd64 only) + name: Build Next.js static output (amd64 only, avoids QEMU for Node) uses: docker/build-push-action@v6 with: context: ./src/frontend @@ -90,19 +90,18 @@ jobs: load: true tags: calendars-builder:local - - name: Extract SSG build output + name: Extract static output run: | docker create --name extract calendars-builder:local docker cp extract:/home/frontend/apps/calendars/out ./src/frontend/out docker rm extract - - name: Build and push nginx image (multi-arch) + name: Build and push frontend image (multi-arch) uses: docker/build-push-action@v6 with: context: ./src/frontend - file: ./src/frontend/Dockerfile.nginx + file: ./src/frontend/Dockerfile.caddy platforms: ${{ github.event_name != 'pull_request' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} - build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/front-dependencies-installation.yml b/.github/workflows/front-dependencies-installation.yml index 642f4d3..77abd79 100644 --- a/.github/workflows/front-dependencies-installation.yml +++ b/.github/workflows/front-dependencies-installation.yml @@ -5,7 +5,7 @@ on: inputs: node_version: required: false - default: '22.x' + default: '24.x' type: string jobs: @@ -19,7 +19,7 @@ jobs: id: front-node_modules with: path: "src/frontend/**/node_modules" - key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }} + key: front-node_modules-${{ hashFiles('src/frontend/**/package-lock.json') }} - name: Setup Node.js if: steps.front-node_modules.outputs.cache-hit != 'true' uses: actions/setup-node@v6 @@ -27,10 +27,10 @@ jobs: node-version: ${{ inputs.node_version }} - name: Install dependencies if: steps.front-node_modules.outputs.cache-hit != 'true' - run: cd src/frontend/ && yarn install --frozen-lockfile + run: cd src/frontend/ && npm ci - name: Cache install frontend if: steps.front-node_modules.outputs.cache-hit != 'true' uses: actions/cache@v5 with: path: "src/frontend/**/node_modules" - key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }} + key: front-node_modules-${{ hashFiles('src/frontend/**/package-lock.json') }} diff --git a/.gitignore b/.gitignore index 7be7485..69dc400 100644 --- a/.gitignore +++ b/.gitignore @@ -47,8 +47,9 @@ env.d/terraform compose.override.yml docker/auth/*.local -# npm +# npm / pnpm node_modules +.pnpm-store # Mails src/backend/core/templates/mail/ diff --git a/CLAUDE.md b/CLAUDE.md index 796e948..12a88ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,44 +18,39 @@ In this project, you can create events, invite people to events, create calendar ### Development Setup ```bash make bootstrap # Initial setup: builds containers, runs migrations, starts services -make run # Start all services (backend + frontend containers) -make run-backend # Start backend services only (for local frontend development) +make start # Start all services (backend + frontend containers) +make start-back # Start backend services only (for local frontend development) make stop # Stop all containers make down # Stop and remove containers, networks, volumes +make update # Update project after pulling changes ``` ### Backend Development ```bash make test-back -- path/to/test.py::TestClass::test_method # Run specific test make test-back-parallel # Run all tests in parallel -make lint # Run ruff + pylint +make lint # Run all linters (back + front) +make lint-back # Run back-end linters only make migrate # Run Django migrations make makemigrations # Create new migrations -make shell # Django shell -make dbshell # PostgreSQL shell +make shell-back-django # Django shell +make shell-db # PostgreSQL shell ``` ### Frontend Development ```bash -make frontend-development-install # Install frontend dependencies locally -make run-frontend-development # Run frontend locally (after run-backend) -make frontend-lint # Run ESLint on frontend -cd src/frontend/apps/calendars && yarn test # Run frontend tests -cd src/frontend/apps/calendars && yarn test:watch # Watch mode +make install-front # Install frontend dependencies +make lint-front # Run ESLint on frontend +make typecheck-front # Run TypeScript type checker +make test-front # Run frontend tests +cd src/frontend/apps/calendars && npm test # Run frontend tests (local) +cd src/frontend/apps/calendars && npm run test:watch # Watch mode (local) ``` ### E2E Tests ```bash -make run-tests-e2e # Run all e2e tests -make run-tests-e2e -- --project chromium --headed # Run with specific browser -``` - -### Internationalization -```bash -make i18n-generate # Generate translation files -make i18n-compile # Compile translations -make crowdin-upload # Upload sources to Crowdin -make crowdin-download # Download translations from Crowdin +make test-e2e # Run all e2e tests +make test-e2e -- --project chromium --headed # Run with specific browser ``` ## Architecture @@ -70,14 +65,14 @@ make crowdin-download # Download translations from Crowdin - `tests/` - pytest test files ### Frontend Structure (`src/frontend/`) -Yarn workspaces monorepo: +npm workspaces: - `apps/calendars/` - Main Next.js application - `src/features/` - Feature modules (calendar, auth, api, i18n, etc.) - `src/pages/` - Next.js pages - `src/hooks/` - Custom React hooks - `apps/e2e/` - Playwright end-to-end tests -### CalDAV Server (`docker/sabredav/`) +### CalDAV Server (`src/caldav/`) PHP SabreDAV server providing CalDAV protocol support, running against the shared PostgreSQL database. **IMPORTANT: Never query the SabreDAV database tables directly from Django.** Always interact with CalDAV through the SabreDAV HTTP API (PROPFIND, REPORT, PUT, etc.). @@ -86,14 +81,13 @@ PHP SabreDAV server providing CalDAV protocol support, running against the share | Service | URL / Port | Description | |---------|------------|-------------| -| **Frontend** | [http://localhost:8920](http://localhost:8920) | Next.js Calendar frontend | -| **Backend API** | [http://localhost:8921](http://localhost:8921) | Django REST API | -| **CalDAV** | [http://localhost:8922](http://localhost:8922) | SabreDAV CalDAV server | -| **Nginx** | [http://localhost:8923](http://localhost:8923) | Reverse proxy (frontend + API) | -| **Redis** | 8924 | Cache and Celery broker | -| **Keycloak** | [http://localhost:8925](http://localhost:8925) | OIDC identity provider | -| **PostgreSQL** | 8926 | Database server | -| **Mailcatcher** | [http://localhost:8927](http://localhost:8927) | Email testing interface | +| **Frontend** | [http://localhost:8930](http://localhost:8930) | Next.js Calendar frontend | +| **Backend API** | [http://localhost:8931](http://localhost:8931) | Django REST API | +| **CalDAV** | [http://localhost:8932](http://localhost:8932) | SabreDAV CalDAV server | +| **Redis** | 8934 | Cache and Celery broker | +| **Keycloak** | [http://localhost:8935](http://localhost:8935) | OIDC identity provider | +| **PostgreSQL** | 8936 | Database server | +| **Mailcatcher** | [http://localhost:8937](http://localhost:8937) | Email testing interface | ## Key Technologies diff --git a/Makefile b/Makefile index a67ee89..5e874aa 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,3 @@ -# /!\ /!\ /!\ /!\ /!\ /!\ /!\ DISCLAIMER /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ -# -# This Makefile is only meant to be used for DEVELOPMENT purpose as we are -# changing the user id that will run in the container. -# -# PLEASE DO NOT USE IT FOR YOUR CI/PRODUCTION/WHATEVER... -# -# /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ -# # Note to developers: # # While editing this file, please respect the following statements: @@ -26,35 +17,24 @@ BOLD := \033[1m RESET := \033[0m GREEN := \033[1;32m - - -# -- Database - -DB_HOST = postgresql -DB_PORT = 5432 +BLUE := \033[1;34m # -- Docker # Get the current user ID to use for docker run and docker exec commands -DOCKER_UID = $(shell id -u) -DOCKER_GID = $(shell id -g) -DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID) -COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose -COMPOSE_EXEC = $(COMPOSE) exec -COMPOSE_EXEC_APP = $(COMPOSE_EXEC) backend-dev -COMPOSE_RUN = $(COMPOSE) run --rm -COMPOSE_RUN_APP = $(COMPOSE_RUN) backend-dev -COMPOSE_RUN_APP_NO_DEPS = $(COMPOSE_RUN) --no-deps backend-dev - -COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin +DOCKER_UID = $(shell id -u) +DOCKER_GID = $(shell id -g) +DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID) +COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose +COMPOSE_EXEC = $(COMPOSE) exec +COMPOSE_EXEC_APP = $(COMPOSE_EXEC) backend-dev +COMPOSE_RUN = $(COMPOSE) run --rm +COMPOSE_RUN_APP = $(COMPOSE_RUN) backend-dev +COMPOSE_RUN_APP_NO_DEPS = $(COMPOSE_RUN) --no-deps backend-dev # -- Backend -MANAGE = $(COMPOSE_RUN_APP) python manage.py -MANAGE_EXEC = $(COMPOSE_EXEC_APP) python manage.py -PSQL_E2E = ./bin/postgres_e2e - -# -- Frontend -PATH_FRONT = ./src/frontend -PATH_FRONT_CALENDARS = $(PATH_FRONT)/apps/calendars +MANAGE = $(COMPOSE_RUN_APP) python manage.py +MANAGE_EXEC = $(COMPOSE_EXEC_APP) python manage.py +PSQL_E2E = ./bin/postgres_e2e # ============================================================================== # RULES @@ -71,7 +51,6 @@ data/static: create-env-files: ## Create empty .local env files for local development create-env-files: \ - env.d/development/crowdin.local \ env.d/development/postgresql.local \ env.d/development/keycloak.local \ env.d/development/backend.local \ @@ -84,13 +63,12 @@ env.d/development/%.local: @echo "# Add your local-specific environment variables below:" >> $@ @echo "# Example: DJANGO_DEBUG=True" >> $@ @echo "" >> $@ -.PHONY: env.d/development/%.local create-docker-network: ## create the docker network if it doesn't exist @docker network create lasuite-network || true .PHONY: create-docker-network -bootstrap: ## Prepare Docker images for the project +bootstrap: ## Prepare the project for local development bootstrap: \ data/media \ data/static \ @@ -99,84 +77,56 @@ bootstrap: \ create-docker-network \ migrate \ migrate-caldav \ - back-i18n-compile \ - run + start .PHONY: bootstrap +update: ## Update the project with latest changes + @$(MAKE) data/media + @$(MAKE) data/static + @$(MAKE) create-env-files + @$(MAKE) build + @$(MAKE) migrate + @$(MAKE) migrate-caldav + @$(MAKE) install-frozen-front +.PHONY: update + # -- Docker/compose + build: cache ?= # --no-cache build: ## build the project containers - @$(MAKE) build-backend cache=$(cache) - @$(MAKE) build-frontend cache=$(cache) - @$(MAKE) build-caldav cache=$(cache) + @$(COMPOSE) build $(cache) .PHONY: build -build-backend: cache ?= -build-backend: ## build the backend-dev container - @$(COMPOSE) build backend-dev $(cache) -.PHONY: build-backend - -build-caldav: cache ?= -build-caldav: ## build the caldav container - @$(COMPOSE) build caldav $(cache) -.PHONY: build-caldav - -build-frontend: cache ?= -build-frontend: ## build the frontend container - @$(COMPOSE) build frontend-dev $(cache) -.PHONY: build-frontend-development - down: ## stop and remove containers, networks, images, and volumes @$(COMPOSE) down rm -rf data/postgresql.* .PHONY: down -logs: ## display backend-dev logs (follow mode) - @$(COMPOSE) logs -f backend-dev +logs: ## display all services logs (follow mode) + @$(COMPOSE) logs -f .PHONY: logs -run-backend: ## start the backend container - @$(COMPOSE) up --force-recreate -d celery-dev - @$(COMPOSE) up --force-recreate -d nginx -.PHONY: run-backend +start: ## start all development services + @$(COMPOSE) up --force-recreate -d worker-dev frontend-dev +.PHONY: start -bootstrap-e2e: ## bootstrap the backend container for e2e tests, without frontend -bootstrap-e2e: \ - data/media \ - data/static \ - create-env-local-files \ - build-backend \ - create-docker-network \ - back-i18n-compile \ - run-backend-e2e -.PHONY: bootstrap-e2e +start-back: ## start backend services only (for local frontend development) + @$(COMPOSE) up --force-recreate -d worker-dev +.PHONY: start-back -clear-db-e2e: ## quickly clears the database for e2e tests, used in the e2e tests - $(PSQL_E2E) -c "$$(cat bin/clear_db_e2e.sql)" -.PHONY: clear-db-e2e +status: ## an alias for "docker compose ps" + @$(COMPOSE) ps +.PHONY: status -run-backend-e2e: ## start the backend container for e2e tests, always remove the postgresql.e2e volume first - @$(MAKE) stop - rm -rf data/postgresql.e2e - @ENV_OVERRIDE=e2e $(MAKE) run-backend - @ENV_OVERRIDE=e2e $(MAKE) migrate -.PHONY: run-backend-e2e +stop: ## stop all development services + @$(COMPOSE) stop +.PHONY: stop -run-tests-e2e: ## run the e2e tests, example: make run-tests-e2e -- --project chromium --headed - @$(MAKE) run-backend-e2e - @args="$(filter-out $@,$(MAKECMDGOALS))" && \ - cd src/frontend/apps/e2e && yarn test $${args:-${1}} -.PHONY: run-tests-e2e - -backend-exec-command: ## execute a command in the backend container - @args="$(filter-out $@,$(MAKECMDGOALS))" && \ - $(MANAGE_EXEC) $${args} -.PHONY: backend-exec-command - -run: ## start the development server and frontend development -run: - @$(MAKE) run-backend - @$(COMPOSE) up --force-recreate -d frontend-dev +restart: ## restart all development services +restart: \ + stop \ + start +.PHONY: restart migrate-caldav: ## Initialize CalDAV server database schema @echo "$(BOLD)Initializing CalDAV server database schema...$(RESET)" @@ -184,57 +134,50 @@ migrate-caldav: ## Initialize CalDAV server database schema @echo "$(GREEN)CalDAV server initialized$(RESET)" .PHONY: migrate-caldav -status: ## an alias for "docker compose ps" - @$(COMPOSE) ps -.PHONY: status +# -- Linters -stop: ## stop the development server using Docker - @$(COMPOSE) stop -.PHONY: stop - -# -- Backend - -demo: ## flush db then create a demo for load testing purpose - @$(MAKE) resetdb - @$(MANAGE) create_demo -.PHONY: demo - -index: ## index all files to remote search - @$(MANAGE) index -.PHONY: index - -# Nota bene: Black should come after isort just in case they don't agree... -lint: ## lint back-end python sources +lint: ## run all linters lint: \ - lint-ruff-format \ - lint-ruff-check \ - lint-pylint + lint-back \ + lint-front .PHONY: lint -lint-ruff-format: ## format back-end python sources with ruff - @echo 'lint:ruff-format started…' +lint-back: ## run back-end linters (with auto-fix) +lint-back: \ + format-back \ + check-back \ + analyze-back +.PHONY: lint-back + +format-back: ## format back-end python sources with ruff @$(COMPOSE_RUN_APP_NO_DEPS) ruff format . -.PHONY: lint-ruff-format +.PHONY: format-back -lint-ruff-check: ## lint back-end python sources with ruff - @echo 'lint:ruff-check started…' +check-back: ## check back-end python sources with ruff @$(COMPOSE_RUN_APP_NO_DEPS) ruff check . --fix -.PHONY: lint-ruff-check +.PHONY: check-back -lint-pylint: ## lint back-end python sources with pylint only on changed files from main - @echo 'lint:pylint started…' - bin/pylint --diff-only=origin/main -.PHONY: lint-pylint +analyze-back: ## lint all back-end python sources with pylint + @$(COMPOSE_RUN_APP_NO_DEPS) pylint . +.PHONY: analyze-back -test: ## run project tests - @$(MAKE) test-back-parallel +lint-front: ## run the frontend linter + @$(COMPOSE) run --rm frontend-dev sh -c "cd apps/calendars && npm run lint" +.PHONY: lint-front + +typecheck-front: ## run the frontend type checker + @$(COMPOSE) run --rm frontend-dev sh -c "cd apps/calendars && npx tsc --noEmit" +.PHONY: typecheck-front + +# -- Tests + +test: ## run all tests +test: \ + test-back-parallel \ + test-front .PHONY: test -test-back: ## run back-end tests (rebuilds and recreates containers) - @echo "$(BOLD)Rebuilding containers (using Docker cache)...$(RESET)" - @$(MAKE) build-caldav - @echo "$(BOLD)Recreating containers...$(RESET)" - @$(COMPOSE) up -d --force-recreate postgresql caldav +test-back: ## run back-end tests @echo "$(BOLD)Running tests...$(RESET)" @args="$(filter-out $@,$(MAKECMDGOALS))" && \ bin/pytest $${args:-${1}} @@ -245,13 +188,49 @@ test-back-parallel: ## run all back-end tests in parallel bin/pytest -n auto $${args:-${1}} .PHONY: test-back-parallel -makemigrations: ## run django makemigrations for the calendar project. +test-front: ## run the frontend tests + @args="$(filter-out $@,$(MAKECMDGOALS))" && \ + $(COMPOSE) run --rm frontend-dev sh -c "cd apps/calendars && npm test -- $${args:-${1}}" +.PHONY: test-front + +# -- E2E Tests + +bootstrap-e2e: ## bootstrap the backend for e2e tests, without frontend +bootstrap-e2e: \ + data/media \ + data/static \ + create-env-files \ + build \ + create-docker-network \ + start-back-e2e +.PHONY: bootstrap-e2e + +clear-db-e2e: ## quickly clears the database for e2e tests + $(PSQL_E2E) -c "$$(cat bin/clear_db_e2e.sql)" +.PHONY: clear-db-e2e + +start-back-e2e: ## start the backend for e2e tests + @$(MAKE) stop + rm -rf data/postgresql.e2e + @ENV_OVERRIDE=e2e $(MAKE) start-back + @ENV_OVERRIDE=e2e $(MAKE) migrate +.PHONY: start-back-e2e + +test-e2e: ## run the e2e tests, example: make test-e2e -- --project chromium --headed + @$(MAKE) start-back-e2e + @args="$(filter-out $@,$(MAKECMDGOALS))" && \ + cd src/frontend/apps/e2e && npm test $${args:-${1}} +.PHONY: test-e2e + +# -- Backend + +makemigrations: ## run django makemigrations @echo "$(BOLD)Running makemigrations$(RESET)" @$(COMPOSE) up -d postgresql @$(MANAGE) makemigrations .PHONY: makemigrations -migrate: ## run django migrations for the calendar project. +migrate: ## run django migrations @echo "$(BOLD)Running migrations$(RESET)" @$(COMPOSE) up -d postgresql @$(MANAGE) migrate @@ -262,81 +241,57 @@ superuser: ## Create an admin superuser with password "admin" @$(MANAGE) createsuperuser --email admin@example.com --password admin .PHONY: superuser +shell-back: ## open a shell in the backend container + @$(COMPOSE) run --rm --build backend-dev /bin/sh +.PHONY: shell-back -back-i18n-compile: ## compile the gettext files - @$(MANAGE) compilemessages --ignore=".venv/**/*" -.PHONY: back-i18n-compile +exec-back: ## open a shell in the running backend-dev container + @$(COMPOSE) exec backend-dev /bin/sh +.PHONY: exec-back -back-lock: ## regenerate the uv.lock file (uses temporary container) +shell-back-django: ## connect to django shell + @$(MANAGE) shell +.PHONY: shell-back-django + +back-lock: ## regenerate the uv.lock file @echo "$(BOLD)Regenerating uv.lock$(RESET)" @docker run --rm -v $(PWD)/src/backend:/app -w /app ghcr.io/astral-sh/uv:python3.13-alpine uv lock .PHONY: back-lock -back-i18n-generate: ## create the .pot files used for i18n - @$(MANAGE) makemessages -a --keep-pot --all -.PHONY: back-i18n-generate - -back-shell: ## open a shell in the backend container - @$(COMPOSE) run --rm --build backend-dev /bin/sh -.PHONY: back-shell - -shell: ## connect to django shell - @$(MANAGE) shell #_plus -.PHONY: shell - # -- Database -dbshell: ## connect to database shell - docker compose exec backend-dev python manage.py dbshell -.PHONY: dbshell +shell-db: ## connect to database shell + @$(COMPOSE) exec backend-dev python manage.py dbshell +.PHONY: shell-db -resetdb: FLUSH_ARGS ?= -resetdb: ## flush database and create a superuser "admin" +reset-db: FLUSH_ARGS ?= +reset-db: ## flush database @echo "$(BOLD)Flush database$(RESET)" @$(MANAGE) flush $(FLUSH_ARGS) - @${MAKE} superuser -.PHONY: resetdb +.PHONY: reset-db -# -- Internationalization +demo: ## flush db then create a demo + @$(MAKE) reset-db + @$(MANAGE) create_demo +.PHONY: demo -crowdin-download: ## Download translated message from crowdin - @$(COMPOSE_RUN_CROWDIN) download -c crowdin/config.yml -.PHONY: crowdin-download +# -- Frontend -crowdin-download-sources: ## Download sources from Crowdin - @$(COMPOSE_RUN_CROWDIN) download sources -c crowdin/config.yml -.PHONY: crowdin-download-sources +install-front: ## install the frontend dependencies + @$(COMPOSE) run --rm frontend-dev sh -c "npm install" +.PHONY: install-front -crowdin-upload: ## Upload source translations to crowdin - @$(COMPOSE_RUN_CROWDIN) upload sources -c crowdin/config.yml -.PHONY: crowdin-upload - -i18n-compile: ## compile all translations -i18n-compile: \ - back-i18n-compile \ - frontend-i18n-compile -.PHONY: i18n-compile - -i18n-generate: ## create the .pot files and extract frontend messages -i18n-generate: \ - back-i18n-generate \ - frontend-i18n-generate -.PHONY: i18n-generate - -i18n-download-and-compile: ## download all translated messages and compile them to be used by all applications -i18n-download-and-compile: \ - crowdin-download \ - i18n-compile -.PHONY: i18n-download-and-compile - -i18n-generate-and-upload: ## generate source translations for all applications and upload them to Crowdin -i18n-generate-and-upload: \ - i18n-generate \ - crowdin-upload -.PHONY: i18n-generate-and-upload +install-frozen-front: ## install frontend dependencies from lockfile + @echo "Installing frontend dependencies..." + @$(COMPOSE) run --rm frontend-dev sh -c "npm ci" +.PHONY: install-frozen-front +shell-front: ## open a shell in the frontend container + @$(COMPOSE) run --rm frontend-dev /bin/sh +.PHONY: shell-front # -- Misc + clean: ## restore repository state as it was freshly cloned git clean -idx .PHONY: clean @@ -350,31 +305,3 @@ help: @echo "Please use 'make $(BOLD)target$(RESET)' where $(BOLD)target$(RESET) is one of:" @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(GREEN)%-30s$(RESET) %s\n", $$1, $$2}' .PHONY: help - -# Front -frontend-development-install: ## install the frontend locally - cd $(PATH_FRONT_CALENDARS) && yarn -.PHONY: frontend-development-install - -frontend-lint: ## run the frontend linter - cd $(PATH_FRONT) && yarn lint -.PHONY: frontend-lint - -run-frontend-development: ## Run the frontend in development mode - @$(COMPOSE) stop frontend-dev - cd $(PATH_FRONT_CALENDARS) && yarn dev -.PHONY: run-frontend-development - -frontend-i18n-extract: ## Extract the frontend translation inside a json to be used for crowdin - cd $(PATH_FRONT) && yarn i18n:extract -.PHONY: frontend-i18n-extract - -frontend-i18n-generate: ## Generate the frontend json files used for crowdin -frontend-i18n-generate: \ - crowdin-download-sources \ - frontend-i18n-extract -.PHONY: frontend-i18n-generate - -frontend-i18n-compile: ## Format the crowin json files used deploy to the apps - cd $(PATH_FRONT) && yarn i18n:deploy -.PHONY: frontend-i18n-compile diff --git a/Procfile b/Procfile index 92aba31..3f9f442 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bin/scalingo_run_web -worker: celery -A calendars.celery_app worker --task-events --beat -l INFO -c $DJANGO_CELERY_CONCURRENCY -Q celery,default +worker: python worker.py postdeploy: source bin/export_pg_vars.sh && python manage.py migrate && SQL_DIR=/app/sabredav/sql bash sabredav/init-database.sh diff --git a/README.md b/README.md index bed28b5..fbadbee 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,10 @@ Compose](https://docs.docker.com/compose/install) installed on your laptop: ```bash $ docker -v - Docker version 27.5.1, build 9f9e405 + Docker version 27.x $ docker compose version - Docker Compose version v2.32.4 + Docker Compose version v2.x ``` > ⚠️ You may need to run the following commands with `sudo` but this can be @@ -73,14 +73,14 @@ The easiest way to start working on the project is to use GNU Make: $ make bootstrap ``` -This command builds the `backend-dev` and `frontend-dev` containers, installs dependencies, performs -database migrations and compile translations. It's a good idea to use this -command each time you are pulling code from the project repository to avoid -dependency-related or migration-related issues. +This command builds the containers, installs dependencies, and runs database +migrations. It's a good idea to use this command each time you are pulling +code from the project repository to avoid dependency-related or +migration-related issues. Your Docker services should now be up and running! 🎉 -You can access the project by going to . +You can access the project by going to . You will be prompted to log in. The default credentials are: @@ -92,7 +92,7 @@ password: calendars Note that if you need to run them afterward, you can use the eponym Make rule: ```bash -$ make run +$ make start ``` You can check all available Make rules using: @@ -101,30 +101,30 @@ You can check all available Make rules using: $ make help ``` -⚠️ For the frontend developer, it is often better to run the frontend in development mode locally. +⚠️ For frontend developers, it is often better to run the frontend in development mode locally. -To do so, install the frontend dependencies with the following command: +First, install the frontend dependencies: -```shellscript -$ make frontend-development-install +```bash +$ make install-front ``` -And run the frontend locally in development mode with the following command: +Then start the backend services: -```shellscript -$ make run-frontend-development +```bash +$ make start-back ``` -To start all the services, except the frontend container, you can use the following command: +And run the frontend locally in development mode: -```shellscript -$ make run-backend +```bash +$ cd src/frontend/apps/calendars && npm run dev ``` ### Django admin You can access the Django admin site at -[http://localhost:8921/admin](http://localhost:8921/admin). +[http://localhost:8931/admin](http://localhost:8931/admin). You first need to create a superuser account: diff --git a/bin/pylint b/bin/pylint deleted file mode 100755 index f58bdb8..0000000 --- a/bin/pylint +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck source=bin/_config.sh -source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" - -declare diff_from -declare -a paths -declare -a args - -# Parse options -for arg in "$@" -do - case $arg in - --diff-only=*) - diff_from="${arg#*=}" - shift - ;; - -*) - args+=("$arg") - shift - ;; - *) - paths+=("$arg") - shift - ;; - esac -done - -if [[ -n "${diff_from}" ]]; then - # Run pylint only on modified files located in src/backend - # (excluding deleted files and migration files) - # shellcheck disable=SC2207 - paths=($(git diff "${diff_from}" --name-only --diff-filter=d -- src/backend ':!**/migrations/*.py' | grep -E '^src/backend/.*\.py$')) -fi - -# Fix docker vs local path when project sources are mounted as a volume -read -ra paths <<< "$(echo "${paths[@]}" | sed "s|src/backend/||g")" -_dc_run --no-deps backend-dev pylint "${paths[@]}" "${args[@]}" diff --git a/bin/scalingo_postfrontend b/bin/scalingo_postfrontend index 848310a..2c4434d 100644 --- a/bin/scalingo_postfrontend +++ b/bin/scalingo_postfrontend @@ -5,14 +5,27 @@ set -o pipefail # don't ignore exit codes when piping output echo "-----> Running post-frontend script" -# Move the frontend build to the nginx root and clean up +# Move the frontend build to the app root and clean up mkdir -p build/ mv src/frontend/apps/calendars/out build/frontend-out cp src/frontend/apps/calendars/src/features/i18n/translations.json translations.json mv src/backend/* ./ -mv src/nginx/* ./ + +# Download Caddy binary with checksum verification +CADDY_VERSION="2.11.2" +CADDY_SHA256="94391dfefe1f278ac8f387ab86162f0e88d87ff97df367f360e51e3cda3df56f" +CADDY_TAR="/tmp/caddy.tar.gz" +curl -fsSL -o "$CADDY_TAR" \ + "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz" +echo "${CADDY_SHA256} ${CADDY_TAR}" | sha256sum -c - +tar -xz -C bin/ caddy < "$CADDY_TAR" +rm "$CADDY_TAR" +chmod +x bin/caddy + +# Copy Caddyfile (uses {$ENV} vars natively, no ERB needed) +cp src/proxy/Caddyfile ./Caddyfile echo "3.13" > .python-version @@ -25,22 +38,25 @@ mkdir -p "$DEB_DIR" "$PHP_PREFIX" # Hardcoded Launchpad URLs for PHP 8.3.6-0maysync1 (Ubuntu Noble amd64) # Source: https://launchpad.net/ubuntu/noble/amd64/php8.3-fpm/8.3.6-0maysync1 -declare -A PHP_DEBS=( - [php8.3-cli]="http://launchpadlibrarian.net/724872605/php8.3-cli_8.3.6-0maysync1_amd64.deb" - [php8.3-fpm]="http://launchpadlibrarian.net/724872610/php8.3-fpm_8.3.6-0maysync1_amd64.deb" - [php8.3-common]="http://launchpadlibrarian.net/724872606/php8.3-common_8.3.6-0maysync1_amd64.deb" - [php8.3-opcache]="http://launchpadlibrarian.net/724872623/php8.3-opcache_8.3.6-0maysync1_amd64.deb" - [php8.3-readline]="http://launchpadlibrarian.net/724872627/php8.3-readline_8.3.6-0maysync1_amd64.deb" - [php8.3-pgsql]="http://launchpadlibrarian.net/724872624/php8.3-pgsql_8.3.6-0maysync1_amd64.deb" - [php8.3-xml]="http://launchpadlibrarian.net/724872633/php8.3-xml_8.3.6-0maysync1_amd64.deb" - [php8.3-mbstring]="http://launchpadlibrarian.net/724872617/php8.3-mbstring_8.3.6-0maysync1_amd64.deb" - [php8.3-curl]="http://launchpadlibrarian.net/724872607/php8.3-curl_8.3.6-0maysync1_amd64.deb" - [php-common]="http://launchpadlibrarian.net/710804987/php-common_93ubuntu2_all.deb" +# Format: "package_name url sha256" +PHP_DEBS=( + "php8.3-cli http://launchpadlibrarian.net/724872605/php8.3-cli_8.3.6-0maysync1_amd64.deb 8cb7461dd06fb214b30c060b80b1c6f95d1ff5e2656fdadf215e50b9f299f196" + "php8.3-fpm http://launchpadlibrarian.net/724872610/php8.3-fpm_8.3.6-0maysync1_amd64.deb b3a9435025766bcbf6c16199c06481c5196098c084933dfabf8867c982edc2b2" + "php8.3-common http://launchpadlibrarian.net/724872606/php8.3-common_8.3.6-0maysync1_amd64.deb 0e0d0ad9c17add5fb2afcc14c6fffb81c2beb99114108b8ebd0461d910a79dfc" + "php8.3-opcache http://launchpadlibrarian.net/724872623/php8.3-opcache_8.3.6-0maysync1_amd64.deb 13b2662201c57904c1eda9b048b1349acaf3609c7d9e8df5b2d93833a059bdb0" + "php8.3-readline http://launchpadlibrarian.net/724872627/php8.3-readline_8.3.6-0maysync1_amd64.deb 380f8ed79196914ee2eebb68bf518a752204826af1fdb8a0d5c9609c76086b90" + "php8.3-pgsql http://launchpadlibrarian.net/724872624/php8.3-pgsql_8.3.6-0maysync1_amd64.deb b1ed204c980c348d1870cfa88c1b40257621ae5696a2a7f44f861a9d00eb7477" + "php8.3-xml http://launchpadlibrarian.net/724872633/php8.3-xml_8.3.6-0maysync1_amd64.deb 6c6ded219d1966a50108d032b7a522e641765a8a6aa48747483313fa7dafd533" + "php8.3-mbstring http://launchpadlibrarian.net/724872617/php8.3-mbstring_8.3.6-0maysync1_amd64.deb 42c89945eb105c2232ab208b893ef65e9abc8af5c95aa10c507498655ef812c4" + "php8.3-curl http://launchpadlibrarian.net/724872607/php8.3-curl_8.3.6-0maysync1_amd64.deb 95d46a22e6b493ba0b6256cf036a2a37d4b9b5f438968073709845af1c17df4c" + "php-common http://launchpadlibrarian.net/710804987/php-common_93ubuntu2_all.deb 39b15c407700e81ddd62580736feba31b187ffff56f6835dac5fa8f847c42529" ) -for pkg in "${!PHP_DEBS[@]}"; do +for entry in "${PHP_DEBS[@]}"; do + read -r pkg url sha256 <<< "$entry" echo " Downloading ${pkg}" - curl -fsSL -o "$DEB_DIR/${pkg}.deb" "${PHP_DEBS[$pkg]}" + curl -fsSL -o "$DEB_DIR/${pkg}.deb" "$url" + echo "${sha256} ${DEB_DIR}/${pkg}.deb" | sha256sum -c - done for deb in "$DEB_DIR"/*.deb; do @@ -90,11 +106,14 @@ chmod +x bin/php echo "-----> PHP version: $("$PHP_PREFIX/usr/bin/php8.3" -n -c "$BUILD_INI" -v | head -1)" echo "-----> PHP modules: $("$PHP_PREFIX/usr/bin/php8.3" -n -c "$BUILD_INI" -m | tr '\n' ' ')" -# Download Composer and install SabreDAV dependencies +# Download Composer with integrity verification and install SabreDAV dependencies echo "-----> Installing SabreDAV dependencies" +COMPOSER_VERSION="2.9.5" +COMPOSER_SHA256="c86ce603fe836bf0861a38c93ac566c8f1e69ac44b2445d9b7a6a17ea2e9972a" curl -fsSL -o bin/composer.phar \ - https://getcomposer.org/download/latest-stable/composer.phar -cp -r docker/sabredav sabredav + "https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar" +echo "${COMPOSER_SHA256} bin/composer.phar" | sha256sum -c - +cp -r src/caldav sabredav cd sabredav "../$PHP_PREFIX/usr/bin/php8.3" -n -c "$BUILD_INI" ../bin/composer.phar install \ --no-dev --optimize-autoloader --no-interaction diff --git a/bin/scalingo_run_web b/bin/scalingo_run_web index 60af21c..859d989 100644 --- a/bin/scalingo_run_web +++ b/bin/scalingo_run_web @@ -3,6 +3,11 @@ # Parse DATABASE_URL into PG* vars for PHP and psql source bin/export_pg_vars.sh +# Set defaults for Caddy env vars +export CALENDARS_FRONTEND_ROOT="${CALENDARS_FRONTEND_ROOT:-/app/build/frontend-out}" +export CALENDARS_FRONTEND_BACKEND_SERVER="${CALENDARS_FRONTEND_BACKEND_SERVER:-localhost:8000}" +export DJANGO_ADMIN_URL="${DJANGO_ADMIN_URL:-admin}" + # Start PHP-FPM for SabreDAV (CalDAV server) .php/usr/sbin/php-fpm8.3 \ -n -c /app/.php/php.ini \ @@ -12,11 +17,11 @@ source bin/export_pg_vars.sh # Start the Django backend gunicorn -b :8000 calendars.wsgi:application --log-file - & -# Start the Nginx server -bin/run & +# Start the Caddy server +bin/caddy run --config Caddyfile --adapter caddyfile & # if the current shell is killed, also terminate all its children -trap "pkill SIGTERM -P $$" SIGTERM +trap "pkill -SIGTERM -P $$" SIGTERM # wait for a single child to finish, wait -n diff --git a/compose.yaml b/compose.yaml index e092066..3073f31 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,7 +5,7 @@ services: postgresql: image: postgres:15 ports: - - "8926:5432" + - "8936:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] interval: 1s @@ -18,12 +18,12 @@ services: redis: image: redis:5 ports: - - "8924:6379" + - "8934:6379" mailcatcher: image: sj26/mailcatcher:latest ports: - - "8927:1080" + - "8937:1080" backend-dev: build: @@ -40,7 +40,7 @@ services: - env.d/development/backend.defaults - env.d/development/backend.local ports: - - "8921:8000" + - "8931:8000" volumes: - ./src/backend:/app - ./data/static:/data/static @@ -57,18 +57,18 @@ services: condition: service_started redis: condition: service_started - celery-dev: + worker-dev: condition: service_started caldav: condition: service_started - celery-dev: + worker-dev: user: ${DOCKER_USER:-1000} image: calendars:backend-development networks: - default - lasuite - command: [ "celery", "-A", "calendars.celery_app", "worker", "-l", "DEBUG" ] + command: [ "python", "worker.py", "-v", "2" ] environment: - DJANGO_CONFIGURATION=Development env_file: @@ -80,28 +80,13 @@ services: - ./src/frontend/apps/calendars/src/features/i18n/translations.json:/data/translations.json:ro - /app/.venv - nginx: - image: nginx:1.25 - ports: - - "8923:8083" - networks: - default: {} - lasuite: - aliases: - - nginx - volumes: - - ./docker/files/development/etc/nginx/conf.d:/etc/nginx/conf.d:ro - depends_on: - - keycloak - - backend-dev - frontend-dev: user: "${DOCKER_USER:-1000}" build: context: src/frontend target: calendars-dev args: - API_ORIGIN: "http://localhost:8921" + API_ORIGIN: "http://localhost:8931" image: calendars:frontend-development env_file: - env.d/development/frontend.defaults @@ -111,20 +96,10 @@ services: - /home/frontend/node_modules - /home/frontend/apps/calendars/node_modules ports: - - "8920:3000" - - crowdin: - image: crowdin/cli:3.16.0 - volumes: - - ".:/app" - env_file: - - env.d/development/crowdin.defaults - - env.d/development/crowdin.local - user: "${DOCKER_USER:-1000}" - working_dir: /app + - "8930:3000" node: - image: node:22 + image: node:24 user: "${DOCKER_USER:-1000}" environment: HOME: /tmp @@ -134,17 +109,17 @@ services: # CalDAV Server caldav: build: - context: docker/sabredav + context: src/caldav ports: - - "8922:80" + - "8932:80" env_file: - env.d/development/caldav.defaults - env.d/development/caldav.local volumes: - - ./docker/sabredav/server.php:/var/www/sabredav/server.php - - ./docker/sabredav/src:/var/www/sabredav/src - - ./docker/sabredav/sql:/var/www/sabredav/sql - - ./docker/sabredav/init-database.sh:/usr/local/bin/init-database.sh + - ./src/caldav/server.php:/var/www/sabredav/server.php + - ./src/caldav/src:/var/www/sabredav/src + - ./src/caldav/sql:/var/www/sabredav/sql + - ./src/caldav/init-database.sh:/usr/local/bin/init-database.sh networks: - default - lasuite @@ -166,13 +141,13 @@ services: - start-dev - --features=preview - --import-realm - - --hostname=http://localhost:8925 + - --hostname=http://localhost:8935 - --hostname-strict=false env_file: - env.d/development/keycloak.defaults - env.d/development/keycloak.local ports: - - "8925:8080" + - "8935:8080" depends_on: postgresql: condition: service_healthy diff --git a/docker/auth/realm.json b/docker/auth/realm.json index 13cd002..59c00ec 100644 --- a/docker/auth/realm.json +++ b/docker/auth/realm.json @@ -694,16 +694,16 @@ "clientAuthenticatorType": "client-secret", "secret": "ThisIsAnExampleKeyForDevPurposeOnly", "redirectUris": [ - "http://localhost:8920/*", - "http://localhost:8921/*", - "http://localhost:8922/*", - "http://localhost:8923/*" + "http://localhost:8930/*", + "http://localhost:8931/*", + "http://localhost:8932/*", + "http://localhost:8933/*" ], "webOrigins": [ - "http://localhost:8920", - "http://localhost:8921", - "http://localhost:8922", - "http://localhost:8923" + "http://localhost:8930", + "http://localhost:8931", + "http://localhost:8932", + "http://localhost:8933" ], "notBefore": 0, "bearerOnly": false, @@ -719,7 +719,7 @@ "access.token.lifespan": "-1", "client.secret.creation.time": "1707820779", "user.info.response.signature.alg": "RS256", - "post.logout.redirect.uris": "http://localhost:8920/*##http://localhost:8921/*", + "post.logout.redirect.uris": "http://localhost:8930/*##http://localhost:8931/*", "oauth2.device.authorization.grant.enabled": "false", "use.jwks.url": "false", "backchannel.logout.revoke.offline.tokens": "false", @@ -765,16 +765,16 @@ "clientAuthenticatorType": "client-secret", "secret": "ThisIsAnExampleKeyForDevPurposeOnly", "redirectUris": [ - "http://localhost:8920/*", - "http://localhost:8921/*", - "http://localhost:8922/*", - "http://localhost:8923/*" + "http://localhost:8930/*", + "http://localhost:8931/*", + "http://localhost:8932/*", + "http://localhost:8933/*" ], "webOrigins": [ - "http://localhost:8920", - "http://localhost:8921", - "http://localhost:8922", - "http://localhost:8923" + "http://localhost:8930", + "http://localhost:8931", + "http://localhost:8932", + "http://localhost:8933" ], "notBefore": 0, "bearerOnly": false, @@ -790,7 +790,7 @@ "access.token.lifespan": "-1", "client.secret.creation.time": "1707820779", "user.info.response.signature.alg": "RS256", - "post.logout.redirect.uris": "http://localhost:8920/*##http://localhost:8921/*", + "post.logout.redirect.uris": "http://localhost:8930/*##http://localhost:8931/*", "oauth2.device.authorization.grant.enabled": "false", "use.jwks.url": "false", "backchannel.logout.revoke.offline.tokens": "false", diff --git a/docker/files/development/etc/nginx/conf.d/default.conf b/docker/files/development/etc/nginx/conf.d/default.conf deleted file mode 100644 index 8b932d7..0000000 --- a/docker/files/development/etc/nginx/conf.d/default.conf +++ /dev/null @@ -1,45 +0,0 @@ -server { - listen 8083; - server_name localhost; - charset utf-8; - - # API routes - proxy to Django backend - location /api/ { - proxy_pass http://backend-dev:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # RSVP and iCal routes - proxy to Django backend - location /rsvp/ { - proxy_pass http://backend-dev:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /ical/ { - proxy_pass http://backend-dev:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Frontend - proxy to Next.js dev server - location / { - proxy_pass http://frontend-dev:3000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket support for HMR - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } -} diff --git a/docker/files/etc/nginx/conf.d/default.conf b/docker/files/etc/nginx/conf.d/default.conf deleted file mode 100644 index a8f7f7d..0000000 --- a/docker/files/etc/nginx/conf.d/default.conf +++ /dev/null @@ -1,15 +0,0 @@ - -server { - listen 8923; - server_name localhost; - charset utf-8; - - # Keycloak - all auth-related paths - location / { - proxy_pass http://keycloak:8080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} diff --git a/docker/sabredav/sql/pgsql.principals.sql b/docker/sabredav/sql/pgsql.principals.sql deleted file mode 100644 index 5a65260..0000000 --- a/docker/sabredav/sql/pgsql.principals.sql +++ /dev/null @@ -1,30 +0,0 @@ -CREATE TABLE principals ( - id SERIAL NOT NULL, - uri VARCHAR(200) NOT NULL, - email VARCHAR(80), - displayname VARCHAR(80) -); - -ALTER TABLE ONLY principals - ADD CONSTRAINT principals_pkey PRIMARY KEY (id); - -CREATE UNIQUE INDEX principals_ukey - ON principals USING btree (uri); - -CREATE TABLE groupmembers ( - id SERIAL NOT NULL, - principal_id INTEGER NOT NULL, - member_id INTEGER NOT NULL -); - -ALTER TABLE ONLY groupmembers - ADD CONSTRAINT groupmembers_pkey PRIMARY KEY (id); - -CREATE UNIQUE INDEX groupmembers_ukey - ON groupmembers USING btree (principal_id, member_id); - -INSERT INTO principals (uri,email,displayname) VALUES -('principals/admin', 'admin@example.org','Administrator'), -('principals/admin/calendar-proxy-read', null, null), -('principals/admin/calendar-proxy-write', null, null); - diff --git a/docker/sabredav/src/AutoCreatePrincipalBackend.php b/docker/sabredav/src/AutoCreatePrincipalBackend.php deleted file mode 100644 index 7c2361c..0000000 --- a/docker/sabredav/src/AutoCreatePrincipalBackend.php +++ /dev/null @@ -1,53 +0,0 @@ - "user@example.com") - $username = substr($path, strlen('principals/')); - - // Create principal directly in database - // Access protected pdo property from parent - $pdo = $this->pdo; - $tableName = $this->tableName; - - try { - $stmt = $pdo->prepare( - 'INSERT INTO ' . $tableName . ' (uri, email, displayname) VALUES (?, ?, ?) ON CONFLICT (uri) DO NOTHING' - ); - $stmt->execute([$path, $username, $username]); - - // Retry getting the principal - $principal = parent::getPrincipalByPath($path); - } catch (\Exception $e) { - // If creation fails, return null - error_log("Failed to auto-create principal: " . $e->getMessage()); - return null; - } - } - - return $principal; - } -} diff --git a/docker/sabredav/src/ICSImportPlugin.php b/docker/sabredav/src/ICSImportPlugin.php deleted file mode 100644 index a1d2169..0000000 --- a/docker/sabredav/src/ICSImportPlugin.php +++ /dev/null @@ -1,278 +0,0 @@ -caldavBackend = $caldavBackend; - $this->importApiKey = $importApiKey; - } - - public function getPluginName() - { - return 'ics-import'; - } - - public function initialize(Server $server) - { - $this->server = $server; - // Priority 90: runs before the debug logger (50) - $server->on('method:POST', [$this, 'httpPost'], 90); - } - - /** - * Handle POST requests with ?import query parameter. - * - * @return bool|null false to stop event propagation, null to let - * other handlers proceed. - */ - public function httpPost($request, $response) - { - // Only handle requests with ?import in the query string - $queryParams = $request->getQueryParameters(); - if (!array_key_exists('import', $queryParams)) { - return; - } - - // Verify the dedicated import header - $headerValue = $request->getHeader('X-Calendars-Import'); - if (!$headerValue || $headerValue !== $this->importApiKey) { - $response->setStatus(403); - $response->setHeader('Content-Type', 'application/json'); - $response->setBody(json_encode([ - 'error' => 'Forbidden: missing or invalid X-Calendars-Import header', - ])); - return false; - } - - // Resolve the calendar from the request path. - // getPath() returns a path relative to the base URI, e.g. - // "calendars/user@example.com/cal-uuid" - $path = $request->getPath(); - $parts = explode('/', trim($path, '/')); - - // Expect exactly: [calendars, , ] - if (count($parts) < 3 || $parts[0] !== 'calendars') { - error_log("[ICSImportPlugin] Invalid calendar path: " . $path); - $response->setStatus(400); - $response->setHeader('Content-Type', 'application/json'); - $response->setBody(json_encode([ - 'error' => 'Invalid calendar path', - ])); - return false; - } - - $principalUser = urldecode($parts[1]); - $calendarUri = $parts[2]; - $principalUri = 'principals/' . $principalUser; - - // Look up calendarId by iterating the user's calendars - $calendarId = $this->resolveCalendarId($principalUri, $calendarUri); - if ($calendarId === null) { - $response->setStatus(404); - $response->setHeader('Content-Type', 'application/json'); - $response->setBody(json_encode([ - 'error' => 'Calendar not found', - ])); - return false; - } - - // Read and parse the raw ICS body - $icsBody = $request->getBodyAsString(); - if (empty($icsBody)) { - $response->setStatus(400); - $response->setHeader('Content-Type', 'application/json'); - $response->setBody(json_encode([ - 'error' => 'Empty request body', - ])); - return false; - } - - try { - $vcal = VObject\Reader::read($icsBody); - } catch (\Exception $e) { - error_log("[ICSImportPlugin] Failed to parse ICS: " . $e->getMessage()); - $response->setStatus(400); - $response->setHeader('Content-Type', 'application/json'); - $response->setBody(json_encode([ - 'error' => 'Failed to parse ICS file', - ])); - return false; - } - - // Validate and auto-repair (fixes missing VALARM ACTION, etc.) - $vcal->validate(VObject\Component::REPAIR); - - // Split by UID using the stream-based splitter - // The splitter expects a stream, so we wrap the serialized data - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $vcal->serialize()); - rewind($stream); - - $splitter = new VObject\Splitter\ICalendar($stream); - - $totalEvents = 0; - $importedCount = 0; - $duplicateCount = 0; - $skippedCount = 0; - $errors = []; - - while ($splitVcal = $splitter->getNext()) { - $totalEvents++; - - try { - // Extract UID from the first VEVENT - $uid = null; - foreach ($splitVcal->VEVENT as $vevent) { - if (isset($vevent->UID)) { - $uid = (string)$vevent->UID; - break; - } - } - - if (!$uid) { - $uid = \Sabre\DAV\UUIDUtil::getUUID(); - } - - // Sanitize event data (strip attachments, truncate descriptions) - // and enforce max resource size - $this->sanitizeAndCheckSize($splitVcal); - - $objectUri = $uid . '.ics'; - $data = $splitVcal->serialize(); - - $this->caldavBackend->createCalendarObject( - $calendarId, - $objectUri, - $data - ); - $importedCount++; - } catch (\Exception $e) { - $msg = $e->getMessage(); - $summary = ''; - if (isset($splitVcal->VEVENT) && isset($splitVcal->VEVENT->SUMMARY)) { - $summary = (string)$splitVcal->VEVENT->SUMMARY; - } - - // Duplicate key (SQLSTATE 23505) = event already exists - // "no valid instances" = dead recurring event (all occurrences excluded) - // Neither is actionable by the user, skip silently. - if (strpos($msg, '23505') !== false) { - $duplicateCount++; - } elseif (strpos($msg, 'valid instances') !== false) { - $skippedCount++; - } else { - $skippedCount++; - if (count($errors) < 10) { - $errors[] = [ - 'uid' => $uid ?? 'unknown', - 'summary' => $summary, - 'error' => $msg, - ]; - } - error_log( - "[ICSImportPlugin] Failed to import event " - . "uid=" . ($uid ?? 'unknown') - . " summary={$summary}: {$msg}" - ); - } - } - } - - fclose($stream); - - error_log( - "[ICSImportPlugin] Import complete: " - . "{$importedCount} imported, " - . "{$duplicateCount} duplicates, " - . "{$skippedCount} failed " - . "out of {$totalEvents} total" - ); - - $response->setStatus(200); - $response->setHeader('Content-Type', 'application/json'); - $response->setBody(json_encode([ - 'total_events' => $totalEvents, - 'imported_count' => $importedCount, - 'duplicate_count' => $duplicateCount, - 'skipped_count' => $skippedCount, - 'errors' => $errors, - ])); - - return false; - } - - /** - * Sanitize a split VCALENDAR before import and enforce max resource size. - * - * Delegates to CalendarSanitizerPlugin (if registered). Import bypasses - * the HTTP layer (uses createCalendarObject directly), so beforeCreateFile - * hooks don't fire — we must call the sanitizer explicitly. - * - * @throws \Exception if the sanitized object exceeds the max resource size. - */ - private function sanitizeAndCheckSize(VObject\Component\VCalendar $vcal) - { - $sanitizer = $this->server->getPlugin('calendar-sanitizer'); - if ($sanitizer) { - $sanitizer->sanitizeVCalendar($vcal); - $sanitizer->checkResourceSize($vcal); - } - } - - /** - * Resolve the internal calendar ID (the [calendarId, instanceId] pair) - * from a principal URI and calendar URI. - * - * @param string $principalUri e.g. "principals/user@example.com" - * @param string $calendarUri e.g. "a1b2c3d4-..." - * @return array|null The calendarId pair, or null if not found. - */ - private function resolveCalendarId(string $principalUri, string $calendarUri) - { - $calendars = $this->caldavBackend->getCalendarsForUser($principalUri); - - foreach ($calendars as $calendar) { - if ($calendar['uri'] === $calendarUri) { - return $calendar['id']; - } - } - - return null; - } - - public function getPluginInfo() - { - return [ - 'name' => $this->getPluginName(), - 'description' => 'Bulk import events from a multi-event ICS file', - ]; - } -} diff --git a/docs/entitlements.md b/docs/entitlements.md index 1a16500..5cb4991 100644 --- a/docs/entitlements.md +++ b/docs/entitlements.md @@ -5,8 +5,10 @@ checking whether a user is allowed to access the application. It integrates with the DeployCenter API in production and uses a local backend for development. -Unlike La Suite Messages, Calendars only checks `can_access` — there -is no admin permission sync. +Calendars checks two entitlements: +- `can_access`: whether the user can use the app at all +- `can_admin`: whether the user is an admin of their organization + (e.g. can create/delete resources) ## Architecture @@ -18,7 +20,7 @@ is no admin permission sync. │ ┌──────────────▼──────────────────────────────┐ │ UserMeSerializer │ -│ GET /users/me/ → { can_access: bool } │ +│ GET /users/me/ → { can_access, can_admin } │ └──────────────┬──────────────────────────────┘ │ ┌──────────────▼──────────────────────────────┐ @@ -97,6 +99,10 @@ is no admin permission sync. is denied (returns 403). - **Import events is fail-closed**: if the entitlements service is unavailable, ICS import is denied (returns 403). +- **Resource provisioning is fail-closed**: if the entitlements + service is unavailable, resource creation/deletion is denied + (returns 403). The `can_admin` check follows the same pattern + as `can_access` fail-closed checks. - The DeployCenter backend falls back to stale cached data when the API is unavailable. - `EntitlementsUnavailableError` is only raised when the API fails @@ -157,7 +163,7 @@ class MyBackend(EntitlementsBackend): def get_user_entitlements( self, user_sub, user_email, user_info=None, force_refresh=False ): - # Return: {"can_access": bool} + # Return: {"can_access": bool, "can_admin": bool, ...} # Raise EntitlementsUnavailableError on failure. pass ``` @@ -175,7 +181,8 @@ Headers: `X-Service-Auth: Bearer {api_key}` Query parameters include any configured `oidc_claims` extracted from the OIDC user_info response (e.g. `siret`). -Expected response: `{"entitlements": {"can_access": true}}` +Expected response: +`{"entitlements": {"can_access": true, "can_admin": false}}` ## Access control flow diff --git a/docs/invitations.md b/docs/invitations.md index d2a3d67..7b67bb0 100644 --- a/docs/invitations.md +++ b/docs/invitations.md @@ -171,9 +171,9 @@ When an event with attendees is deleted: | Email service | `src/backend/core/services/calendar_invitation_service.py` | | ICS parser | `src/backend/core/services/calendar_invitation_service.py` (`ICalendarParser`) | | Email templates | `src/backend/core/templates/emails/calendar_invitation*.html` | -| SabreDAV sanitizer | `docker/sabredav/src/CalendarSanitizerPlugin.php` | -| SabreDAV attendee dedup | `docker/sabredav/src/AttendeeNormalizerPlugin.php` | -| SabreDAV callback plugin | `docker/sabredav/src/HttpCallbackIMipPlugin.php` | +| SabreDAV sanitizer | `src/caldav/src/CalendarSanitizerPlugin.php` | +| SabreDAV attendee dedup | `src/caldav/src/AttendeeNormalizerPlugin.php` | +| SabreDAV callback plugin | `src/caldav/src/HttpCallbackIMipPlugin.php` | ## Future: Messages mail client integration diff --git a/docs/organizations.md b/docs/organizations.md new file mode 100644 index 0000000..c9a9459 --- /dev/null +++ b/docs/organizations.md @@ -0,0 +1,503 @@ +# Organizations + +How organizations and multi-tenancy work in La Suite Calendars: +scoping users, calendars, resources, and permissions by +organization. + +## Table of Contents + +- [Overview](#overview) +- [What Is an Organization in Calendars?](#what-is-an-organization-in-calendars) +- [Where Organization Context Comes From](#where-organization-context-comes-from) + - [OIDC Claims](#oidc-claims) + - [DeployCenter and Entitlements](#deploycenter-and-entitlements) +- [What Is Org-Scoped](#what-is-org-scoped) +- [Data Model](#data-model) + - [Auto-Population on Login](#auto-population-on-login) + - [Why a Local Model?](#why-a-local-model) +- [CalDAV and Multi-Tenancy](#caldav-and-multi-tenancy) + - [The Core Problem](#the-core-problem) + - [SabreDAV Principal Backend Filtering](#sabredav-principal-backend-filtering) +- [User Discovery and Sharing](#user-discovery-and-sharing) +- [Resource Scoping](#resource-scoping) +- [Entitlements Integration](#entitlements-integration) +- [Frontend Considerations](#frontend-considerations) +- [Implementation Plan](#implementation-plan) + - [Phase 1: Org Context Propagation](#phase-1-org-context-propagation) + - [Phase 2: User Discovery Scoping](#phase-2-user-discovery-scoping) + - [Phase 3: CalDAV Scoping](#phase-3-caldav-scoping) + - [Phase 4: Resource Scoping](#phase-4-resource-scoping) +- [Key Files](#key-files) + +--- + +## Overview + +La Suite Calendars scopes users, calendars, and resources by +organization. Every user belongs to exactly one org, determined by +their email domain (default) or a configurable OIDC claim. Orgs +are created automatically on first login. + +The overarching constraint is that **CalDAV has no native concept of +organizations or tenants**. The protocol operates on principals, +calendars, and scheduling. Org scoping is layered on top via +SabreDAV backend filtering. + +--- + +## What Is an Organization in Calendars? + +An organization is a **boundary for user and resource visibility**. +Within an organization: + +- Users discover and share calendars with other members +- Resources (meeting rooms, equipment) are visible and bookable +- Admins manage resources and org-level settings + +Across organizations: + +- Users cannot discover each other (unless explicitly shared with + by email) +- Resources are invisible +- Scheduling still works via email (iTIP), just like scheduling + with external users + +An organization maps to a real-world entity: a company, a government +agency, a university department. It is identified by a **unique ID** +-- either a specific OIDC claim value (like a SIRET number in +France) or an **email domain** (the default). + +--- + +## Where Organization Context Comes From + +### OIDC Claims + +The user's organization is identified at authentication time via an +OIDC claim. The identity provider (Keycloak) includes an org +identifier in the user info response: + +```json +{ + "sub": "abc-123", + "email": "alice@ministry.gouv.fr", + "siret": "13002526500013" +} +``` + +The claim used to identify the org (e.g. `siret`) is configured via +`OIDC_USERINFO_ORGANIZATION_CLAIM`. When no claim is configured, the email +domain is used as the org identifier. + +The claim names and their meaning depend on the Keycloak +configuration and the identity federation in use (AgentConnect for +French public sector, ProConnect, etc.). + +### DeployCenter and Entitlements + +The [entitlements system](entitlements.md) already forwards +OIDC claims to DeployCenter. The `oidc_claims` parameter in the +DeployCenter backend config specifies which claims to include: + +```json +{ + "oidc_claims": ["siret"] +} +``` + +DeployCenter uses the `siret` claim to determine which organization +the user belongs to and whether they have access to the Calendars +service. It also knows the **organization name** (e.g. "Ministere +X") and returns it in the entitlements response. This means the +entitlements system is the **source of truth for org names** -- +Calendars does not need a separate OIDC claim for the org name. + +--- + +## What Is Org-Scoped + +| Feature | Behavior | +|---------|----------| +| **User discovery** (search when sharing) | Same-org users only | +| **Calendar sharing suggestions** | Same-org users; cross-org by typing full email | +| **Resource discovery** | Same-org resources only | +| **Resource creation** | Org admins only (`can_admin` entitlement) | +| **Resource booking** | Same-org users only | +| **Free/busy lookup** | Same-org principals | + +Things that are **not** org-scoped: + +- **Event scheduling via email**: iTIP works across orgs (same as + external users) +- **Calendar sharing by email**: A user can share a calendar with + anyone by typing their email address +- **CalDAV protocol operations**: PUT, GET, PROPFIND on a user's + own calendars +- **Subscription tokens**: Public iCal URLs + +--- + +## Data Model + +A lightweight `Organization` model stores just enough to scope +data. It is auto-populated on login from the OIDC claim (or email +domain) and the entitlements response. + +```python +class Organization(BaseModel): + """Organization model, populated from OIDC claims and entitlements.""" + name = models.CharField(max_length=200, blank=True) + external_id = models.CharField( + max_length=128, unique=True, db_index=True + ) + + class Meta: + db_table = "calendars_organization" +``` + +A FK on User links each user to their org: + +```python +class User(AbstractBaseUser, ...): + organization = models.ForeignKey( + Organization, on_delete=models.PROTECT, related_name="members" + ) +``` + +### Auto-Population on Login + +On OIDC login, `post_get_or_create_user()` resolves the org. The +org identifier (`external_id`) comes from the OIDC claim or +email domain. The org **name** comes from the entitlements response. + +```python +# 1. Determine the org identifier +claim_key = settings.OIDC_USERINFO_ORGANIZATION_CLAIM # e.g. "siret" +if claim_key: + reg_id = user_info.get(claim_key) +else: + # Default: derive org from email domain + reg_id = user.email.split("@")[-1] if user.email and "@" in user.email else None + +# 2. Get org name from entitlements (looked up from DeployCenter) +org_name = entitlements.get("organization_name", "") + +# 3. Create or update the org +if reg_id: + org, created = Organization.objects.get_or_create( + external_id=reg_id, + defaults={"name": org_name} + ) + if not created and org_name and org.name != org_name: + org.name = org_name + org.save(update_fields=["name"]) + if user.organization_id != org.id: + user.organization = org + user.save(update_fields=["organization"]) +``` + +By default, the org is derived from the **user's email domain** +(e.g. `alice@ministry.gouv.fr` → org `ministry.gouv.fr`). Orgs +are always created automatically on first login. + +`OIDC_USERINFO_ORGANIZATION_CLAIM` can override this with a specific OIDC +claim (e.g. `"siret"` for French public sector, `"organization_id"` +for other identity providers). + +The org name is kept in sync: each login updates it from the +entitlements response if it has changed. If entitlements are +unavailable on login (fail-open), the org is still created from +the OIDC claim or email domain, but the name is left empty until +a subsequent login succeeds. + +### Why a Local Model? + +- **Efficient queries**: `User.objects.filter(organization=org)` + for user search scoping, instead of JSONField queries on claims +- **Org-level settings**: Place to attach resource creation policy, + default timezone, branding, etc. +- **SabreDAV integration**: The org's Django UUID is forwarded to + SabreDAV as `X-CalDAV-Organization` for principal scoping +- **Claim-agnostic**: The claim name is a setting, not hardcoded + +--- + +## CalDAV and Multi-Tenancy + +### The Core Problem + +CalDAV principals live in a flat namespace: `principals/{username}`. +When a frontend does a `PROPFIND` on `principals/` or a +`principal-property-search`, SabreDAV returns **all** principals. +There is no built-in way to scope results by organization. + +The same applies to scheduling: `calendar-free-busy-set` returns +free/busy for any principal the server knows about. + +A design principle is that **Django should not inspect or filter +CalDAV traffic**. The `CalDAVProxyView` is a pass-through proxy -- +it sets authentication headers and forwards requests, but never +parses CalDAV XML. Org scoping must happen either in SabreDAV +itself or in the frontend. + +### SabreDAV Principal Backend Filtering + +Org scoping is enforced server-side in SabreDAV by filtering +principal queries by `org_id`. Django never inspects CalDAV +traffic -- it only sets the `X-CalDAV-Organization` header. + +```php +class OrgAwarePrincipalBackend extends AutoCreatePrincipalBackend +{ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + $orgId = $this->server->httpRequest->getHeader('X-CalDAV-Organization'); + // Add WHERE org_id = $orgId to the query + return parent::searchPrincipals(...) + org filter; + } +} +``` + +**Implementation:** +1. Add `org_id` column to `principals` table +2. Set it when auto-creating principals (from `X-CalDAV-Organization`) +3. Filter discovery and listing methods by org (see below) +4. `CalDAVProxyView` always sets `X-CalDAV-Organization` from the + authenticated user's org + +**Which backend methods are org-filtered:** + +| Method | Filtered? | Why | +|--------|-----------|-----| +| `searchPrincipals()` | Yes | Used for user/resource discovery | +| `getPrincipalsByPrefix()` | Yes | Used for listing principals | +| `getPrincipalByPath()` | **No** | Used for sharing and scheduling with a specific principal — must work cross-org | +| Schedule outbox free/busy | Yes | Aggregates all calendars for a principal — scoped to same-org | +| `free-busy-query` on a specific calendar | **No** | If a user has access to a shared calendar, they can query its free/busy regardless of org | + +This keeps principal paths stable (`principals/{username}` -- no +org baked into the URI), enforces scoping at the CalDAV level for +both web and external clients (Apple Calendar, Thunderbird), and +allows cross-org sharing to work when explicitly granted. + +--- + +## User Discovery and Sharing + +When a user types an email to share a calendar, the frontend +currently searches all users. With orgs: + +### Same-Org Discovery + +The user search endpoint (`GET /api/v1.0/users/?q=alice`) should +return only users in the same organization by default. This is a +Django-side filter: + +```python +# In UserViewSet.get_queryset(): +queryset = queryset.filter(organization=request.user.organization) +``` + +### Cross-Org Sharing + +Typing a full email address that doesn't match any same-org user +should still work. The frontend sends the sharing request to CalDAV +with the email address. SabreDAV resolves the recipient via +`getPrincipalByPath()`, which is **not org-filtered** -- so +cross-org sharing works. If the recipient is external (not on this +server), an iTIP email is sent. + +Once shared, the recipient can see that calendar's events and +query its free/busy (via `free-busy-query` on the specific +calendar collection), regardless of org. They still cannot +discover the sharer's other calendars or query their aggregate +free/busy via the scheduling outbox. + +The UI should make this distinction clear: +- Autocomplete results: same-org users +- Manual email entry: "This user is outside your organization" + +### CalDAV User Search + +The CalDAV `principal-property-search` REPORT is how external +CalDAV clients discover users. SabreDAV only returns principals +from the user's org. + +--- + +## Resource Scoping + +Resource discovery and booking scoping follows the same pattern as +user scoping. See [docs/resources.md](resources.md) for the full +resource design. + +Key points for org scoping of resources: + +1. **Resource principals** get an org association (same `org_id` + column on the `principals` table as user principals) +2. **Resource discovery** is scoped to the user's org +3. **Resource creation** requires an org-admin permission +4. **Resource email addresses** follow the convention + `{opaque-id}@resource.calendar.{APP_DOMAIN}` -- the org is + **not** encoded in the email address (see resources.md for + rationale) +5. **No cross-org resource booking** -- the auto-schedule plugin + rejects invitations from users outside the resource's org + +The resource creation permission gate checks the user's +`can_admin` entitlement, returned by the entitlements system +alongside `can_access`. + +--- + +## Entitlements Integration + +The [entitlements system](entitlements.md) controls whether a user +can access Calendars at all. Organizations add a layer on top: + +``` +User authenticates + → Entitlements check: can_access? (DeployCenter, per-user) + → Org resolution: which org? (OIDC claim or email domain) + → Org name: from entitlements response + → Scoping: show only org's users/resources +``` + +The entitlements backend already receives OIDC claims (including +`siret`). DeployCenter resolves the organization and returns the +org name alongside the access decision: + +```json +{ + "can_access": true, + "can_admin": false, + "organization_name": "Ministere X" +} +``` + +On login, `post_get_or_create_user()` uses the entitlements +response to populate the organization name. The org's +`external_id` is determined locally (from the OIDC claim or +email domain), but the **display name comes from DeployCenter**. +This avoids requiring a separate OIDC claim for the org name and +keeps DeployCenter as the single source of truth for org metadata. + +--- + +## Frontend Considerations + +### Org-Aware UI Elements + +- **User search/autocomplete:** Filter results to same-org users by + default; show a "search all users" or "invite by email" option for + cross-org +- **Resource picker:** Only show resources from the user's org +- **Calendar list:** No change (users only see calendars they own or + are shared on) +- **No-access page:** Already exists (from entitlements). Could show + org-specific messaging +- **Org switcher:** Not needed (a user belongs to exactly one org) + +### Org Context in Frontend State + +The frontend needs to know the user's org ID to: +- Scope user search API calls +- Scope resource PROPFIND requests +- Display org name in the UI + +This can come from the `GET /users/me/` response: + +```json +{ + "id": "user-uuid", + "email": "alice@ministry.gouv.fr", + "organization": { + "id": "org-uuid", + "name": "Ministere X" + } +} +``` + +--- + +## Implementation Plan + +### Phase 1: Org Context Propagation + +**Goal:** Every request knows the user's org. + +1. Add `OIDC_USERINFO_ORGANIZATION_CLAIM` setting (default: `""`, uses email + domain) +2. Add `Organization` model (id, name, external_id) +3. Add `organization` FK on `User` (non-nullable -- every user has + an org) +4. In `post_get_or_create_user()`, resolve org from email domain or + OIDC claim and set `user.organization` +5. Expose `organization` in `GET /users/me/` response +6. Frontend stores org context from `/users/me/` + +### Phase 2: User Discovery Scoping + +**Goal:** User search returns same-org users by default. + +1. Scope `UserViewSet` queryset by org when org is set +2. Frontend user search autocomplete uses scoped endpoint +3. Cross-org sharing still works via explicit email entry +4. Add `X-CalDAV-Organization` header to `CalDAVProxyView` requests + +### Phase 3: CalDAV Scoping + +**Goal:** CalDAV operations are org-scoped. + +1. Add `org_id` column to SabreDAV `principals` table +2. Set `org_id` when auto-creating principals (from + `X-CalDAV-Organization`) +3. Extend `AutoCreatePrincipalBackend` to filter + `searchPrincipals()`, `getPrincipalsByPrefix()`, and free/busy + by org +4. Test with external CalDAV clients (Apple Calendar, Thunderbird) + +### Phase 4: Resource Scoping + +**Goal:** Resources are org-scoped (depends on resources being +implemented -- see [docs/resources.md](resources.md)). + +1. Resource creation endpoint requires org-admin permission +2. Resource principals get org association +3. Resource discovery is scoped by org +4. Resource booking respects org boundaries + +--- + +## Key Files + +| Area | Path | +|------|------| +| User model (claims) | `src/backend/core/models.py` | +| OIDC auth backend | `src/backend/core/authentication/backends.py` | +| OIDC settings | `src/backend/calendars/settings.py` | +| CalDAV proxy | `src/backend/core/api/viewsets_caldav.py` | +| Entitlements system | `src/backend/core/entitlements/` | +| User serializer | `src/backend/core/api/serializers.py` | +| SabreDAV principal backend | `src/caldav/src/AutoCreatePrincipalBackend.php` | +| SabreDAV server config | `src/caldav/server.php` | +| Resource scoping details | `docs/resources.md` | +| Entitlements details | `docs/entitlements.md` | + +--- + +## Design Decisions + +1. **A user belongs to exactly one org**, determined by the OIDC + claim at login. +2. **Cross-org calendar sharing is allowed** -- a user can share by + email with anyone. Autocomplete only shows same-org users; + cross-org sharing requires typing the full email. +3. **Cross-org resource booking is not allowed** -- the + auto-schedule plugin rejects invitations from users outside the + resource's org. +4. **Org scoping is enforced in SabreDAV**, not in Django. Django + only sets `X-CalDAV-Organization` on proxied requests. +5. **Org is derived from email domain by default**. A specific OIDC + claim can be configured via `OIDC_USERINFO_ORGANIZATION_CLAIM` (e.g. + `siret` for French public sector). diff --git a/docs/resources.md b/docs/resources.md new file mode 100644 index 0000000..20d1e91 --- /dev/null +++ b/docs/resources.md @@ -0,0 +1,1002 @@ +# Calendar Resources + +This document describes the design and implementation plan for **calendar resources** in La Suite Calendars: meeting rooms, vehicles, projectors, and any other bookable assets that people can reserve alongside events. + +## Table of Contents + +- [Overview](#overview) +- [What Is a Calendar Resource?](#what-is-a-calendar-resource) + - [Resource Scheduling Addresses](#resource-scheduling-addresses-not-real-emails) + - [Email Safety](#email-safety) +- [CalDAV Standards and Interoperability](#caldav-standards-and-interoperability) +- [Data Model](#data-model) +- [Resource Lifecycle](#resource-lifecycle) +- [Booking Flow](#booking-flow) +- [Auto-Scheduling and Conflict Detection](#auto-scheduling-and-conflict-detection) +- [Free/Busy and Availability](#freebusy-and-availability) +- [Access Rights and Administration](#access-rights-and-administration) +- [Sharing and Delegation](#sharing-and-delegation) +- [Resource Discovery](#resource-discovery) +- [Interoperability with CalDAV Clients](#interoperability-with-caldav-clients) +- [What SabreDAV Provides (and What It Doesn't)](#what-sabredav-provides-and-what-it-doesnt) +- [Implementation Plan](#implementation-plan) + - [Phase 1: Resource Principals in SabreDAV](#phase-1-resource-principals-in-sabredav) + - [Phase 2: Django Resource Management](#phase-2-django-resource-management) + - [Phase 3: Auto-Scheduling Plugin](#phase-3-auto-scheduling-plugin) + - [Phase 4: Frontend Resource UI](#phase-4-frontend-resource-ui) + - [Phase 5: Advanced Features](#phase-5-advanced-features) +- [Database Schema Changes](#database-schema-changes) +- [API Design](#api-design) +- [SabreDAV Plugin Design](#sabredav-plugin-design) +- [Frontend Components](#frontend-components) +- [Migration and Deployment](#migration-and-deployment) + +--- + +## Overview + +Calendar resources allow organizations to manage shared physical assets (rooms, vehicles, equipment) through the calendar. Users book resources by adding them as attendees to events, just like inviting a person. The system handles availability checking, conflict prevention, and automatic accept/decline responses. + +**Key principles:** + +- Resources are modeled as CalDAV principals, following RFC 6638 +- Any CalDAV client can book a resource by inviting its email address +- The server handles auto-scheduling (accept if free, decline if busy) +- Resource provisioning (create/delete) happens through Django; all other management through CalDAV +- Double-booking prevention is enforced server-side + +--- + +## What Is a Calendar Resource? + +A calendar resource is a bookable entity that is not a person. Resources fall into two categories defined by the iCalendar standard (RFC 5545): + +| Type | CUTYPE | Examples | +|------|--------|----------| +| **Room** | `ROOM` | Conference rooms, meeting rooms, auditoriums, phone booths | +| **Resource** | `RESOURCE` | Projectors, vehicles, cameras, whiteboards, parking spots | + +Each resource has: +- A **display name** (e.g., "Room 101 - Large Conference") +- A **scheduling address** (a `mailto:` URI used as a CalDAV identifier -- see below) +- A **calendar** that shows its bookings +- **Metadata** (capacity, location, description, equipment list) +- **Availability hours** (e.g., 8:00-20:00 on weekdays) +- An **auto-schedule policy** (auto-accept, require approval, etc.) +- One or more **administrators** who manage the resource + +### Resource Scheduling Addresses (Not Real Emails) + +In CalDAV, every principal (user or resource) is identified by a `mailto:` URI in the `calendar-user-address-set` property. This is how the scheduling protocol matches an `ATTENDEE` on an event to a principal on the server. **These addresses do not need to be real, routable email addresses.** + +When SabreDAV receives a scheduling request for `mailto:c_a1b2c3d4@resource.calendar.example.com`, it looks up the `email` column in the `principals` table. If a matching principal is found locally, the iTIP message is delivered **internally** (inbox-to-inbox on the same server). See [Email Safety](#email-safety) for how outbound emails to resource addresses are prevented. + +**Recommended convention**: `{opaque-id}@resource.calendar.{APP_DOMAIN}` + +The address uses: +- An **opaque identifier** (UUID or short hash, prefixed with `c_`) rather than a human-readable slug, so renaming a resource doesn't change its address +- A **subdomain you control** (`resource.calendar.{APP_DOMAIN}`), which avoids collisions with real user emails and leaves the door open for future inbound email (e.g., resources acting as meeting organizers) + +Examples (with `APP_DOMAIN=example.com`): +- `c_a1b2c3d4@resource.calendar.example.com` +- `c_f6g7h8i9@resource.calendar.example.com` + +By default, no MX record is configured for this subdomain, so inbound email silently fails -- same practical effect as a non-routable address, but reversible if resources need to send/receive email in the future (e.g., room-initiated meetings with external attendees). + +Org scoping is handled at the CalDAV/application level, not encoded in the email address. + +The system identifies resource principals via the `calendar_user_type` column in the SabreDAV principals table, not by email pattern. + +### Email Safety + +Resource scheduling addresses are **not real mailboxes**. No email should ever be sent to or from them. However, the current invitation system (`HttpCallbackIMipPlugin` -> Django -> email) will attempt to email any attendee unless explicitly prevented. This must be handled at two levels: + +#### Internal: Preventing Outbound Emails to Resources + +In the current flow, when a user adds attendees to an event, `HttpCallbackIMipPlugin` POSTs to Django for each attendee, and Django sends an invitation email. Without intervention, Django would attempt to email the resource address. + +**Fix 1 (SabreDAV)**: The `ResourceAutoSchedulePlugin` (priority 120) sets `$message->scheduleStatus` on the iTIP message before `HttpCallbackIMipPlugin` runs. The base `IMipPlugin` class skips messages that already have a status set, preventing the callback entirely. + +**Fix 2 (Django, safety net)**: `CalendarInvitationService.send_invitation()` checks if the recipient is a resource address (by checking `calendar_user_type` of the principal or matching the `@resource.calendar.example.com` domain) and skips email sending. + +Both should be implemented. The plugin is the primary gate; Django is the fallback. + +#### External: What Happens When Outside Systems See the Address + +When an event includes both a resource and external attendees, the ICS attachment in invitation emails lists all `ATTENDEE` properties, including the resource's `mailto:c_abc123@resource.calendar.example.com`. This is visible to external recipients. Here's why this is safe: + +| Scenario | What happens | Risk | +|----------|-------------|------| +| External email client tries to send iTIP REPLY | REPLY goes to ORGANIZER only (the human), not to other attendees | None | +| External email client tries to contact all attendees | Email to `@resource.calendar.example.com` bounces (no MX record by default) | None | +| External CalDAV server tries to book the resource | iTIP REQUEST email to `@resource.calendar.example.com` bounces | None -- resources are only bookable through this server | +| CalDAV client connected to THIS server | Uses `SCHEDULE-AGENT=SERVER`; all scheduling goes through SabreDAV, no email | None | + +By default no MX record exists for `resource.calendar.{APP_DOMAIN}`, so external email attempts bounce harmlessly. All legitimate resource interactions go through the CalDAV server. If resources need to send/receive email in the future (e.g., room-initiated meetings), an MX record can be added and inbound mail routed to the application. + +--- + +## CalDAV Standards and Interoperability + +### Relevant Standards + +| Standard | Role | +|----------|------| +| **RFC 5545** (iCalendar) | Defines `CUTYPE` parameter (`ROOM`, `RESOURCE`) on `ATTENDEE` properties | +| **RFC 6638** (CalDAV Scheduling) | Defines `calendar-user-type` DAV property on principals; scheduling transport (inbox/outbox); `SCHEDULE-AGENT` parameter | +| **RFC 4791** (CalDAV) | Calendar collections, `free-busy-query` REPORT | +| **RFC 7953** (Calendar Availability) | `VAVAILABILITY` component for defining operating hours (future) | +| **draft-cal-resource-schema** | Resource metadata schema (capacity, booking window, manager, etc.) | +| **draft-pot-caldav-sharing** | Calendar sharing between principals | + +### How Resources Work in CalDAV + +In CalDAV, a resource is a **regular principal** with a special `calendar-user-type` property set to `ROOM` or `RESOURCE`. It has its own calendar home, schedule inbox, and schedule outbox, exactly like a user principal. + +When a user creates an event with a resource as an attendee: + +``` +ATTENDEE;CUTYPE=ROOM;ROLE=NON-PARTICIPANT;PARTSTAT=NEEDS-ACTION; + RSVP=TRUE:mailto:c_a1b2c3d4@resource.calendar.example.com +``` + +The CalDAV scheduling server (RFC 6638): +1. Detects the `ATTENDEE` on the event +2. Delivers an iTIP `REQUEST` to the resource's schedule inbox +3. A server-side agent checks the resource's calendar for conflicts +4. Sends an iTIP `REPLY` back with `PARTSTAT=ACCEPTED` or `PARTSTAT=DECLINED` + +**Auto-scheduling is not standardized** -- it is a server implementation feature. RFC 6638 only defines the transport mechanism. This project implements auto-scheduling as a custom SabreDAV plugin (see [What SabreDAV Provides](#what-sabredav-provides-and-what-it-doesnt) for rationale). + +### What This Means for Interoperability + +The critical insight: **resource booking does not require client-side support**. Any CalDAV client that can add an attendee by email address can book a resource. The server handles everything else. This means: + +- Apple Calendar, Thunderbird, GNOME Calendar, and all CalDAV clients work out of the box +- Clients that support `CUTYPE` can display resources differently from people +- Clients that support `principal-property-search` can discover available resources +- The web frontend provides the richest experience (resource browser, availability view) + +--- + +## Data Model + +### Design Principle: CalDAV as Single Source of Truth + +Resources are **entirely managed in CalDAV** -- metadata, calendar data, and access control. No Django model is needed. + +- **Metadata** (name, capacity, location, equipment, etc.): DAV properties via PROPFIND/PROPPATCH +- **Calendar data** (bookings, free/busy): CalDAV calendar collections +- **Access control**: CalDAV sharing (`CS:share`) with privilege levels, the same mechanism already used for user calendar sharing +- **Scheduling config** (auto-schedule mode, booking policies): Custom DAV properties read by the SabreDAV plugin + +Django's role is limited to: +- **Provisioning**: A REST endpoint to create/delete resource principals. CalDAV has no standard operation to create a principal, so this is the one justified exception to the "Django is a pass-through" rule. Django makes CalDAV requests to SabreDAV to set up the principal + calendar + initial properties. +- **Proxying**: The existing CalDAV proxy (`CalDAVProxyView`) forwards all CalDAV requests, including those for resource principals + +This means **zero new Django models** for resources. The frontend manages resource permissions via `CalDavService.shareCalendar()` / `getCalendarSharees()`, exactly like it already does for user calendars. + +### Resource as a CalDAV Principal + +Each resource exists as a principal in SabreDAV with a single +dedicated calendar: + +``` +principals/resources/{resource-id} + -> calendar-home-set: /calendars/resources/{resource-id}/ + -> schedule-inbox-URL: /calendars/resources/{resource-id}/inbox/ + -> schedule-outbox-URL: /calendars/resources/{resource-id}/outbox/ + -> calendar-user-type: ROOM | RESOURCE + -> calendar-user-address-set: mailto:{opaque-id}@resource.calendar.example.com +``` + +The `mailto:` address is a CalDAV internal identifier, not a real +mailbox (see [Resource Scheduling Addresses](#resource-scheduling-addresses-not-real-emails)). + +**A resource principal has exactly one calendar.** Although CalDAV +allows any principal to own multiple calendar collections, this +doesn't make sense for resources (a room has one schedule). +`MKCALENDAR` requests targeting a resource principal's calendar +home are rejected by a SabreDAV plugin (hooking +`beforeMethod:MKCALENDAR`). The single calendar is created during +provisioning and cannot be added to or removed independently. + +### Resource Properties (CalDAV) + +All resource metadata is stored as DAV properties on the resource's principal or default calendar, using standard properties where they exist and a project namespace (`{urn:lasuite:calendars}`) for the rest. + +SabreDAV's `PropertyStorage` plugin persists custom properties in a `propertystorage` table automatically -- any property set via PROPPATCH is stored and returned via PROPFIND. + +#### Standard Properties (on the principal or calendar collection) + +| Property | Namespace | Where | Description | +|----------|-----------|-------|-------------| +| `displayname` | `{DAV:}` | Principal | Resource name | +| `calendar-user-type` | `{urn:ietf:params:xml:ns:caldav}` | Principal | `ROOM` or `RESOURCE` | +| `calendar-description` | `{urn:ietf:params:xml:ns:caldav}` | Calendar | Free-form description | +| `calendar-color` | `{http://apple.com/ns/ical/}` | Calendar | Hex color (e.g., `#4CAF50`) | +| `calendar-timezone` | `{urn:ietf:params:xml:ns:caldav}` | Calendar | VTIMEZONE component | + +#### Semi-Standard Properties (Apple CalendarServer) + +| Property | Namespace | Where | Description | +|----------|-----------|-------|-------------| +| `capacity` | `{http://calendarserver.org/ns/}` | Principal | Integer, number of seats/units | + +#### Custom Properties (Project Namespace) + +| Property | Namespace | Where | Type | Description | +|----------|-----------|-------|------|-------------| +| `location` | `{urn:lasuite:calendars}` | Principal | String | Building, floor, address | +| `equipment` | `{urn:lasuite:calendars}` | Principal | JSON array | `["Projector", "Whiteboard"]` | +| `tags` | `{urn:lasuite:calendars}` | Principal | JSON array | `["building-a", "video"]` | +| `auto-schedule-mode` | `{urn:lasuite:calendars}` | Principal | String | See auto-schedule modes below | +| `is-active` | `{urn:lasuite:calendars}` | Principal | Boolean | `true` / `false` | +| `restricted-access` | `{urn:lasuite:calendars}` | Principal | Boolean | Restrict booking to explicit access | +| `multiple-bookings` | `{urn:lasuite:calendars}` | Principal | Integer | Max concurrent bookings (1 = no overlap) | +| `max-booking-duration` | `{urn:lasuite:calendars}` | Principal | Duration | ISO 8601 (e.g., `PT4H`) | +| `booking-window-start` | `{urn:lasuite:calendars}` | Principal | Duration | How far ahead (e.g., `P90D`) | +| `booking-window-end` | `{urn:lasuite:calendars}` | Principal | Duration | Minimum notice (e.g., `PT1H`) | + +#### Example: Reading Resource Properties + +```xml +PROPFIND /principals/resources/room-101/ + + + + + + + + + + + + + +``` + +#### Example: Setting Resource Properties + +```xml +PROPPATCH /principals/resources/room-101/ + + + + + 20 + Building A, Floor 2 + ["Projector", "Whiteboard", "Video conferencing"] + ["video-conference", "whiteboard", "building-a"] + automatic + + + +``` + +### Auto-Schedule Modes + +Stored as the `{urn:lasuite:calendars}auto-schedule-mode` property on the resource principal. Inspired by Apple Calendar Server's proven model: + +| Mode | Behavior | +|------|----------| +| `automatic` | Accept if free, decline if busy (default) | +| `accept-always` | Accept all invitations regardless of conflicts | +| `decline-always` | Decline all invitations (resource offline) | +| `manual` | Require a resource manager to accept/decline | + +The `ResourceAutoSchedulePlugin` reads this property from SabreDAV's `propertystorage` table (same PostgreSQL instance) during scheduling -- no Django roundtrip needed. + +### Resource Access (CalDAV Sharing) + +Resource access uses the **same CalDAV sharing mechanism** (`CS:share`) already used for user calendar sharing. No Django model is needed. + +The resource's calendar is shared with administrators/managers using the existing privilege levels: + +| CalDAV Privilege | Role | Can Do | +|-----------------|------|--------| +| `read` | Viewer | See bookings, free/busy | +| `read-write` | Manager | See bookings, modify/cancel any booking | +| `admin` | Admin | Full control: edit properties, manage sharing, override bookings | + +The frontend manages this via `CalDavService.shareCalendar()` and `getCalendarSharees()` -- the same code already used for sharing user calendars. + +The resource creator is automatically the calendar owner (the principal itself owns the calendar collection). Additional admins are added via sharing. + +### What About Search and Filtering? + +Storing metadata in CalDAV raises the question: how do you filter resources by capacity, tags, or location? + +For most deployments, the number of resources is small (tens to low hundreds). The frontend can: + +1. **PROPFIND** on `principals/resources/` to fetch all resource principals with their properties in a single request +2. **Filter and sort client-side** in JavaScript (capacity >= 10, tags include "projector", etc.) + +This is simple, avoids data duplication, and works well up to ~1000 resources. If a deployment needs SQL-level search across thousands of resources, a Django read-only index synced from CalDAV can be added later as an optimization -- but this is not needed for v1. + +--- + +## Resource Lifecycle + +### Creating a Resource + +1. An **org admin** (`can_admin` entitlement) calls the Django + REST API to create a resource (name, type) +2. Django calls SabreDAV's `/internal-api/resources/` endpoint + (POST with JSON body). The `InternalApiPlugin` handles both + principal creation and default calendar creation atomically. + The admin's `org_id` is passed in the request body. +3. The frontend sets additional metadata via PROPPATCH (capacity, + location, equipment, etc.) +4. The resource is immediately available for booking + +No Django model is created -- the CalDAV principal **is** the +resource. + +> **Architecture note:** All resource CRUD goes through the +> `/internal-api/` namespace in SabreDAV, which is completely +> separate from the CalDAV protocol namespace. This avoids direct +> database access from Django to SabreDAV tables. The internal API +> is gated by the `X-Internal-Api-Key` header (different from the +> `X-Api-Key` used by the CalDAV proxy) and is explicitly blocked +> by the Django proxy's path validation. + +### Updating a Resource + +All metadata changes go through CalDAV `PROPPATCH` directly (from the frontend or via a Django proxy endpoint). There is no Django model to keep in sync. + +- Display properties (name, color, description): PROPPATCH on the calendar collection +- Resource properties (capacity, equipment, location, tags): PROPPATCH on the principal +- Scheduling config (auto-schedule-mode, booking policies): PROPPATCH on the principal +- Deactivating a resource: set `{urn:lasuite:calendars}is-active` to `false` + +### Deleting a Resource + +1. Django calls `DELETE /internal-api/resources/{resource-id}` on SabreDAV +2. The `InternalApiPlugin` deletes all calendars, calendar objects, + scheduling objects, and the principal row atomically +3. Sharing entries are automatically cleaned up with the calendar + +Existing events in user calendars that reference the resource as +an attendee are **left as-is**. The resource's `mailto:` address +becomes an unresolvable address -- same as if an external attendee +disappeared. The resource will simply stop responding to scheduling +requests. This avoids the complexity of modifying events across +all user calendars. + +--- + +## Booking Flow + +### Standard Booking (Any CalDAV Client) + +``` +User CalDAV Server Auto-Scheduler + | | | + |-- PUT event ------------>| | + | (ATTENDEE=c_...@resource.calendar.example.com) + | |-- iTIP REQUEST --------->| + | | |-- check free/busy + | | |-- no conflict? + | |<-- iTIP REPLY -----------| + | | (PARTSTAT=ACCEPTED) | + |<-- schedule-status ------| | + | (1.2 = delivered) | | +``` + +### Web Frontend Booking + +The web UI provides a richer experience: + +1. User opens event creation modal +2. User clicks "Add Room" or "Add Resource" +3. A resource picker shows available resources with metadata +4. User selects a resource; frontend checks free/busy in real-time +5. Frontend adds the resource as an `ATTENDEE` with `CUTYPE=ROOM` +6. On save, the CalDAV flow triggers auto-scheduling +7. The event updates with the resource's `PARTSTAT` response + +### Booking with Conflicts + +When a resource is already booked: + +1. User creates event with resource as attendee +2. Auto-scheduler detects conflict +3. Resource declines (`PARTSTAT=DECLINED`) +4. Organizer sees the declined status +5. Frontend shows a warning: "Room 101 is unavailable at this time" + +### Recurring Event Booking + +For recurring events, the auto-scheduler must check **every instance** within a reasonable window (e.g., 1 year) for conflicts. If any instance conflicts: + +- **Option A (strict)**: Decline the entire series +- **Option B (lenient)**: Accept the series but decline specific instances via `EXDATE` + +The recommended approach is **Option A** for simplicity, with the UI helping users find conflict-free times. + +--- + +## What SabreDAV Provides (and What It Doesn't) + +SabreDAV is the most mature open-source CalDAV server, but its support for resource principals (ROOM/RESOURCE) has gaps. Understanding exactly what's built in vs. what we extend is important. + +### CUTYPE: Extensible but Not Enabled by Default + +RFC 6638 §2.4.2 defines the `{urn:ietf:params:xml:ns:caldav}calendar-user-type` property on principals, with values from RFC 5545's CUTYPE parameter (INDIVIDUAL, ROOM, RESOURCE, GROUP, etc.). SabreDAV **acknowledges** this property but doesn't fully implement it: + +- The [`Schedule\Plugin`](https://github.com/sabre-io/dav/blob/master/lib/CalDAV/Schedule/Plugin.php) hardcodes `calendar-user-type` to `'INDIVIDUAL'` for all principals, with an inline comment: *"The server currently reports every principal to be of type INDIVIDUAL."* +- The [`PrincipalBackend\PDO`](https://github.com/sabre-io/dav/blob/master/lib/DAVACL/PrincipalBackend/PDO.php) base class only maps two columns in its `$fieldMap`: `displayname` and `email`. There is no `calendar_user_type` column in the default schema. + +**However**, SabreDAV was designed for this to be extended: + +1. The `$fieldMap` in `PrincipalBackend\PDO` is a `protected` property that subclasses can override to add custom DB columns and map them to WebDAV properties. +2. When a property is in the `$fieldMap`, the `Principal` node exposes it via `getProperties()`, which runs **before** the Schedule Plugin's `handle()` callback. +3. The Schedule Plugin uses `$propFind->handle()` which is a **no-op when the property is already set**. So the hardcoded `'INDIVIDUAL'` only serves as a fallback for principals that lack the column. + +This means adding CUTYPE support is a **one-line extension** of the fieldMap, not a hack: + +```php +// In our AutoCreatePrincipalBackend (extends PrincipalBackend\PDO) +protected $fieldMap = [ + '{DAV:}displayname' => ['dbField' => 'displayname'], + '{http://sabredav.org/ns}email-address' => ['dbField' => 'email'], + // This is all it takes -- SabreDAV handles the rest + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => ['dbField' => 'calendar_user_type'], +]; +``` + +With this in place, `getPrincipalByPath()`, `getPrincipalsByPrefix()`, and `searchPrincipals()` all automatically include the real CUTYPE value. CalDAV clients that support `principal-property-search` (Apple Calendar) can discover rooms and resources. PROPFIND on resource principals returns the correct type. No custom plugin is needed for CUTYPE -- just the standard extension point. + +### Auto-Scheduling: Not Built In Anywhere + +While CUTYPE is a matter of data exposure (and SabreDAV provides the extension points), **auto-scheduling is a different story**. No existing CalDAV server component provides automatic accept/decline for resource principals: + +**SabreDAV Core**: The `Schedule\Plugin` handles iTIP delivery (inbox/outbox) but has no logic to auto-accept or auto-decline based on calendar-user-type or availability. After delivering a scheduling message, it's done. + +**Nextcloud**: Built resource scheduling on top of SabreDAV, but deeply coupled to the Nextcloud framework (`\OCP\` interfaces, DI container, app framework). None of the code (`ResourcePrincipalBackend.php`, `RoomPrincipalBackend.php`, `CalDavBackend.php`) is extractable as a standalone plugin. + +**SOGo**: Has resource auto-accept, but written in Objective-C with its own CalDAV stack -- not SabreDAV-based. + +**Bedework**: The one enterprise CalDAV server with built-in room/resource scheduling, but it's a Java application, not a SabreDAV plugin. + +**Packagist / Open Source**: No third-party SabreDAV resource scheduling package exists. + +### What This Means + +- **CUTYPE support**: Uses SabreDAV's built-in `$fieldMap` extension. One line of code, fully idiomatic. +- **Auto-scheduling**: Requires a custom `ResourceAutoSchedulePlugin` (~350 lines of PHP). Follows the same pattern as our existing `HttpCallbackIMipPlugin`: hook into the `schedule` event, inspect the iTIP message, act on it. The plugin reads resource configuration from the shared PostgreSQL database directly (same instance SabreDAV already uses). + +--- + +## Auto-Scheduling and Conflict Detection + +### How Auto-Scheduling Works + +Auto-scheduling is implemented as a **custom SabreDAV plugin** that intercepts scheduling deliveries to resource principals. It runs after `Sabre\CalDAV\Schedule\Plugin` delivers the iTIP message. No existing SabreDAV plugin provides this functionality (see [What SabreDAV Provides](#what-sabredav-provides-and-what-it-doesnt)). + +```php +class ResourceAutoSchedulePlugin extends ServerPlugin +{ + // Hook into the 'schedule' event + function schedule(ITip\Message $message) + { + // 1. Is the recipient a resource principal? + // 2. What is the resource's auto_schedule_mode? + // 3. For 'automatic' mode: check free/busy + // 4. Set $message->scheduleStatus accordingly + // 5. Update PARTSTAT in the delivered calendar object + } +} +``` + +### Conflict Detection Algorithm + +``` +function hasConflict(resource, newEvent): + // Get the resource's calendar + calendar = resource.getDefaultCalendar() + + // For each instance of the new event (expand recurrence) + for instance in expandInstances(newEvent, maxWindow=1year): + if instance.transp == TRANSPARENT: + continue // transparent events don't block + + // Query existing events in this time range + existing = calendar.getEvents(instance.start, instance.end) + + // Check against max concurrent bookings + overlapping = countOverlapping(existing, instance) + if overlapping >= resource.multiple_bookings: + return true // conflict + + return false // no conflict +``` + +### Edge Cases + +- **All-day events**: Treated as blocking the entire day +- **Tentative events**: Count as busy (configurable per resource) +- **Cancelled events**: Do not count as busy +- **Transparent events** (`TRANSP=TRANSPARENT`): Do not count as busy +- **Recurring with exceptions**: Must check each expanded instance + +--- + +## Free/Busy and Availability + +### Free/Busy Queries + +Resources support standard CalDAV free/busy queries. Two methods: + +**Method 1: `CALDAV:free-busy-query` REPORT** (RFC 4791) + +```xml +REPORT /calendars/resources/room-101/default/ + + + + +``` + +Returns a `VFREEBUSY` component with busy intervals. + +**Method 2: Schedule outbox POST** (RFC 6638) + +The organizer POSTs a `VFREEBUSY` request to their outbox, specifying the resource as an attendee. The server returns the resource's free/busy data. + +### Availability Hours (VAVAILABILITY) — Future + +Resources could define operating hours using RFC 7953 +`VAVAILABILITY` (e.g., Monday-Friday 8:00-20:00, booking outside +these hours auto-declined). However, SabreDAV does not support +RFC 7953 out of the box — the auto-schedule plugin would need to +implement `VAVAILABILITY` parsing. This is deferred to Phase 5. + +### Frontend Availability View + +The web frontend shows: +- A **day/week timeline** of the resource's bookings +- **Color-coded slots**: free (green), busy (red), tentative (yellow) +- A **multi-resource view** to compare several rooms side by side + +--- + +## Access Rights and Administration + +Resource access control uses **CalDAV sharing** (`CS:share`), the same mechanism already used for sharing user calendars. No Django model is needed. + +### Privilege Levels + +| CalDAV Privilege | Role | Capabilities | +|-----------------|------|--------------| +| *(no share)* | Any authenticated user | Discover resource, view free/busy, book by adding as attendee | +| `read` | Shared viewer | All of the above + see full booking details on the calendar | +| `read-write` | Manager | All of the above + modify/cancel any booking on the resource | +| `admin` | Administrator | All of the above + edit resource properties (PROPPATCH), manage sharing, override auto-schedule decisions, delete resource | + +### How It Works + +- The **resource principal** owns its calendar collection. This is the "owner" in CalDAV terms. +- **Admins** are added by sharing the resource's calendar with `admin` privilege via `CS:share` (same as `CalDavService.shareCalendar()`) +- **Managers** get `read-write` privilege +- **Viewers** get `read` privilege +- **Any authenticated user** can book the resource (add it as an attendee to their event) and query free/busy -- this does not require sharing. The CalDAV scheduling protocol handles this via the resource's schedule inbox. + +This maps directly to how user calendar sharing already works in the frontend (`CalendarShareModal`, `CalDavService.shareCalendar()`, `getCalendarSharees()`). + +### Restricted Resources + +For resources that should not be bookable by everyone (executive rooms, specialized equipment), the `{urn:lasuite:calendars}restricted-access` property can be set to `true`. When set, the auto-schedule plugin only accepts invitations from users who have been explicitly shared on the resource's calendar. + +### Organization Scoping and Permissions + +See [docs/organizations.md](organizations.md) for the full org design. Key points for resources: + +- **Resource discovery** is org-scoped: SabreDAV filters resource principals by the `org_id` column on the `principals` table, using the `X-CalDAV-Organization` header set by Django. +- **Cross-org resource booking is not allowed**: the auto-schedule plugin rejects invitations from users outside the resource's org. +- **Resource creation** requires the `can_admin` entitlement (returned by the entitlements system alongside `can_access`). + +--- + +## Sharing and Delegation + +### Delegation to Resource Managers + +When `auto-schedule-mode=manual`, incoming booking requests require approval: + +1. User creates event with resource as attendee +2. Auto-scheduler detects `manual` mode +3. Resource stays in `PARTSTAT=NEEDS-ACTION` +4. Resource managers receive a notification +5. Manager approves or declines via: + - The web UI (resource management panel) + - Direct calendar interaction (change PARTSTAT on the resource's calendar) +6. iTIP REPLY sent back to the organizer + +--- + +## Resource Discovery + +### How Users Find Resources + +In major calendar apps, users pick rooms from a list scoped to their organization. This works because those apps are tied to an organization directory (Workspace domain, Exchange GAL, etc.). In CalDAV, there are two discovery mechanisms: + +### Discovery via CalDAV (`principal-property-search`) + +RFC 3744 defines a `DAV:principal-property-search` REPORT that lets clients search for principals by property. For example, to find all rooms: + +```xml +REPORT /principals/ + + + + + + + ROOM + + + + + + +``` + +**Limitations**: +- Returns **all** matching principals on the server -- there is no built-in scoping by organization or tenant +- Only Apple Calendar uses this for resource discovery in practice +- Thunderbird and GNOME Calendar do not support resource discovery via CalDAV +- CalDAV properties are limited to basic fields (name, email, CUTYPE) -- no capacity, equipment, location metadata + +**In a single-tenant deployment**, this works fine: all resources belong to the same organization, so returning all of them is correct. + +**In a multi-tenant deployment**, SabreDAV filters results by the `org_id` on the `principals` table (see [Organization Scoping](#organization-scoping-and-permissions)). + +### Discovery via CalDAV + Client-Side Filtering (Web Frontend) + +The web frontend fetches all resource principals via a single +`PROPFIND` on `principals/resources/` (through the CalDAV proxy) +and filters/sorts **client-side** in JavaScript. This is the +**primary and richest** discovery method: + +- Full metadata: capacity, equipment, location, tags +- Client-side filtering: by type, capacity, location, tags +- Real-time availability: via `free-busy-query` REPORT +- Org-scoped: SabreDAV filters by `org_id` before returning + +No Django endpoint is needed for resource discovery. The frontend +does the same kind of PROPFIND it already does for calendars. + +### Discovery via Email (All Clients) + +Any CalDAV client can book a resource by typing its scheduling address in the attendee field, even without a discovery UI. This is the universal fallback that works everywhere. + +## Interoperability with CalDAV Clients + +| Client | Book by Email | See CUTYPE | Discover via CalDAV | Free/Busy | +|--------|---------------|------------|---------------------|-----------| +| Apple Calendar | Yes | Yes | Yes (`principal-property-search`) | Yes | +| Thunderbird | Yes | No (shows as person) | No | Yes | +| GNOME Calendar | Yes | Partial | No | Partial | +| Web Frontend | Yes | Yes | Yes (PROPFIND + client-side) | Yes | +| Any CalDAV client | Yes | Varies | Varies | Yes | + +The key takeaway: **booking works universally** (any client can invite a resource by email). **Discovery** (browsing available rooms) is richest in the web frontend and limited in native CalDAV clients. + +--- + +## Implementation Plan + +### Phase 1: Resource Principals in SabreDAV + +**Goal**: Resources exist as CalDAV principals and can receive scheduling messages. + +**Changes**: + +1. **Extend SabreDAV principals table**: Add `calendar_user_type` column (and `org_id` for multi-tenancy) +2. **Extend `AutoCreatePrincipalBackend.$fieldMap`**: Add `{urn:ietf:params:xml:ns:caldav}calendar-user-type` → `calendar_user_type`. This is SabreDAV's idiomatic extension point for principal properties -- the base `PrincipalBackend\PDO` automatically includes mapped fields in all queries, and the `Schedule\Plugin`'s hardcoded `INDIVIDUAL` becomes a fallback (see [What SabreDAV Provides](#what-sabredav-provides-and-what-it-doesnt)) +3. **Add nested principal prefixes**: `principals/users/` for user principals and `principals/resources/` for resource principals, with a custom `CalendarsRoot` node to handle the nested structure (SabreDAV's default `CalendarRoot` only supports flat principal prefixes) +4. **Verify scheduling delivery**: Ensure `Schedule\Plugin` delivers iTIP messages to resource inboxes + +**Files to modify**: +- `src/caldav/sql/pgsql.principals.sql` -- add columns +- `src/caldav/src/AutoCreatePrincipalBackend.php` -- extend `$fieldMap` +- `src/caldav/src/CalendarsRoot.php` -- custom DAV Collection for nested prefixes +- `src/caldav/server.php` -- use CalendarsRoot and nested principal collections + +### Phase 2: Django Provisioning API + +**Goal**: REST endpoint to create/delete resource principals, gated by org-level permissions. + +**Changes**: + +1. **Resource provisioning endpoint**: Creates the SabreDAV principal + calendar via CalDAV requests (no direct DB writes). Checks the user's `can_admin` entitlement. +2. **Resource deletion endpoint**: Cleans up CalDAV principal + calendar. Same permission check. + +No Django model is needed. Metadata is managed via CalDAV PROPPATCH. Access control uses CalDAV sharing (`CS:share`), the same way user calendars are shared. Org-level permission to create/delete resources comes from the `can_admin` entitlement. + +**Files to create/modify**: +- `src/backend/core/api/viewsets.py` -- add resource provisioning viewset +- `src/backend/core/services/resource_service.py` -- provisioning logic (CalDAV calls) + +### Phase 3: Auto-Scheduling Plugin + +**Goal**: Resources automatically accept/decline based on availability. + +No existing SabreDAV plugin provides this -- see [What SabreDAV Provides](#what-sabredav-provides-and-what-it-doesnt). The plugin is ~350 lines of PHP, following the same pattern as the existing `HttpCallbackIMipPlugin`. + +**Changes**: + +1. **`ResourceAutoSchedulePlugin.php`**: SabreDAV plugin that hooks into the `schedule` event (same hook `HttpCallbackIMipPlugin` already uses) +2. **Conflict detection**: Query the resource's calendar for overlapping events +3. **Auto-schedule modes**: Read the `{urn:lasuite:calendars}auto-schedule-mode` property from SabreDAV's `propertystorage` table (same PostgreSQL instance, no Django roundtrip) +4. **iTIP REPLY generation**: Send `ACCEPTED` or `DECLINED` back to organizer +5. **Availability hours**: Not in v1 (see Phase 5) + +**Files to create/modify**: +- `src/caldav/src/ResourceAutoSchedulePlugin.php` -- new plugin +- `src/caldav/server.php` -- register plugin + +**Design choice**: The plugin reads the `auto_schedule_mode` from the database directly (same PostgreSQL instance SabreDAV already uses) rather than calling Django's API, to avoid circular HTTP dependencies during scheduling. + +### Phase 4: Frontend Resource UI + +**Goal**: Users can discover, browse, and book resources from the web interface. + +**Components**: + +1. **Resource directory**: Searchable/filterable list of all resources +2. **Resource detail panel**: Shows metadata, availability timeline, current bookings +3. **Resource picker in event modal**: Add room/resource when creating an event +4. **Availability checker**: Real-time free/busy display when selecting a resource +5. **Multi-resource timeline**: Side-by-side availability comparison +6. **Resource management panel**: For admins to create/edit/configure resources + +**Files to create**: +- `src/frontend/apps/calendars/src/features/resources/` -- new feature module + - `types.ts` -- TypeScript types for resource properties + - `components/ResourceDirectory.tsx` + - `components/ResourcePicker.tsx` + - `components/ResourceDetail.tsx` + - `components/ResourceTimeline.tsx` + - `components/ResourceAdmin.tsx` +- `src/frontend/apps/calendars/src/services/dav/CalDavService.ts` -- extend with resource PROPFIND/PROPPATCH methods +- Updates to event modal for resource attendee support + +The frontend reads/writes resource metadata via CalDAV (PROPFIND/PROPPATCH), the same way it already manages calendar properties. The Django REST API is only used for provisioning (create/delete). + +### Phase 5: Advanced Features + +**Goal**: Polish and power-user features. + +1. **Approval workflow**: Notification system for `manual` mode resources +2. **Booking policies**: Max duration, booking window, recurring limits +3. **Resource groups**: Group rooms by building/floor for easier browsing +4. **Capacity warnings**: Warn when event attendee count exceeds room capacity +5. **Resource calendar overlay**: Show resource bookings in the main calendar view +6. **Reporting**: Usage statistics, popular times, underutilized resources +7. **VAVAILABILITY editor**: UI for configuring resource operating hours +8. **Bulk resource import**: CSV/JSON import for provisioning many resources + +--- + +## Database Schema Changes + +### SabreDAV: Principals Table + +The principals table includes two extra columns beyond the SabreDAV defaults (defined in `src/caldav/sql/pgsql.principals.sql`): + +```sql +CREATE TABLE principals ( + id SERIAL NOT NULL, + uri VARCHAR(200) NOT NULL, + email VARCHAR(80), + displayname VARCHAR(80), + calendar_user_type VARCHAR(20) DEFAULT 'INDIVIDUAL', -- INDIVIDUAL, ROOM, RESOURCE + org_id VARCHAR(200), -- organization scoping + PRIMARY KEY (id), + UNIQUE (uri) +); + +CREATE INDEX idx_principals_org_id ON principals (org_id) WHERE org_id IS NOT NULL; +CREATE INDEX idx_principals_cutype ON principals (calendar_user_type) + WHERE calendar_user_type IN ('ROOM', 'RESOURCE'); +``` + +The `calendar_user_type` column is exposed as the standard `{urn:ietf:params:xml:ns:caldav}calendar-user-type` DAV property via `AutoCreatePrincipalBackend.$fieldMap`. The `org_id` column is used internally for org-scoped filtering and is not exposed as a DAV property. + +Resource metadata (capacity, location, equipment, etc.) is stored in SabreDAV's existing `propertystorage` table via PROPPATCH -- no additional SabreDAV schema changes needed. + +### Django: No New Tables + +No Django models are needed for resources. Access control uses CalDAV sharing (stored in SabreDAV's `calendarinstances` table). Resource metadata uses SabreDAV's `propertystorage` table. Both are managed through CalDAV protocol, not direct database access. + +--- + +## API Design + +Resource metadata is read/written via **CalDAV** (PROPFIND/PROPPATCH). The Django REST API only handles provisioning and deletion. + +### Django REST API (Provisioning) + +``` +POST /api/v1.0/resources/ # Create resource (provision principal + calendar) +DELETE /api/v1.0/resources/{resource-id}/ # Delete resource (cleanup principal + calendar) +``` + +Both `POST` and `DELETE` require the `can_admin` entitlement. Access control (sharing) is managed via CalDAV `CS:share` on the resource's calendar -- no Django access endpoints needed. + +### Create Resource Request + +```json +POST /api/v1.0/resources/ +{ + "name": "Room 101 - Large Conference", + "resource_type": "ROOM" +} +``` + +Django checks the user's `can_admin` entitlement, provisions the CalDAV principal and calendar, then the frontend sets additional metadata via PROPPATCH. + +### CalDAV API (Metadata and Discovery) + +All resource metadata is managed via standard CalDAV protocol: + +| Operation | Method | URL | +|-----------|--------|-----| +| List all resources | `PROPFIND` | `/api/v1.0/caldav/principals/resources/` | +| Get resource properties | `PROPFIND` | `/api/v1.0/caldav/principals/resources/{resource-id}/` | +| Update resource properties | `PROPPATCH` | `/api/v1.0/caldav/principals/resources/{resource-id}/` | +| Get resource calendar | `PROPFIND` | `/api/v1.0/caldav/calendars/resources/{resource-id}/default/` | +| Query free/busy | `REPORT` | `/api/v1.0/caldav/calendars/resources/{resource-id}/default/` | + +The frontend fetches all resource principals with their properties in a single PROPFIND request and filters/sorts client-side. This is the same pattern used for fetching calendars today. + +--- + +## SabreDAV Plugin Design + +CUTYPE support uses SabreDAV's built-in `$fieldMap` extension point (no plugin needed). Auto-scheduling requires a custom plugin because SabreDAV has no built-in resource auto-scheduling, and no reusable third-party implementation exists (see [What SabreDAV Provides](#what-sabredav-provides-and-what-it-doesnt)). + +### ResourceAutoSchedulePlugin + +```php +class ResourceAutoSchedulePlugin extends DAV\ServerPlugin +{ + function getPluginName() { return 'resource-auto-schedule'; } + + function initialize(DAV\Server $server) + { + $server->on('schedule', [$this, 'autoSchedule'], 120); + // Priority 120: runs after Schedule\Plugin (110) + } + + function autoSchedule(ITip\Message $message) + { + // Only handle messages TO resource principals + if (!$this->isResourcePrincipal($message->recipient)) { + return; + } + + // Read auto-schedule-mode from propertystorage table + $mode = $this->getAutoScheduleMode($message->recipient); + + switch ($mode) { + case 'accept_always': + $this->acceptInvitation($message); + break; + case 'decline_always': + $this->declineInvitation($message); + break; + case 'automatic': + if ($this->hasConflict($message)) { + $this->declineInvitation($message); + } else { + $this->acceptInvitation($message); + } + break; + case 'manual': + // Do nothing; leave PARTSTAT=NEEDS-ACTION + // Managers see pending requests on the resource calendar + break; + } + } +} +``` + +### Integration with Existing Plugins + +The plugin runs in this order: +1. `CalendarSanitizerPlugin` (priority 85) -- strips binaries, truncates +2. `AttendeeNormalizerPlugin` (priority 90) -- normalizes emails +3. `CalDAV\Schedule\Plugin` (priority 110) -- delivers iTIP messages +4. **`ResourceAutoSchedulePlugin`** (priority 120) -- auto-accept/decline +5. `HttpCallbackIMipPlugin` -- notifies Django (for email delivery) + +--- + +## Frontend Components + +### Data Flow + +The frontend reads/writes resource metadata via CalDAV, just like it does for calendars: + +``` +CalDavService.fetchResourcePrincipals() → PROPFIND /principals/resources/ +CalDavService.getResourceProperties(id) → PROPFIND /principals/resources/{resource-id}/ +CalDavService.updateResourceProperties() → PROPPATCH /principals/resources/{resource-id}/ +CalDavService.fetchResourceEvents() → REPORT on resource calendar +CalDavService.queryResourceFreeBusy() → free-busy-query REPORT +``` + +The Django REST API is only called for provisioning +(create/delete). Everything else goes through CalDAV. + +### Resource Directory (`/resources`) + +A browsable directory of all resources with: +- **Filter sidebar**: Type (room/resource), capacity range, tags, location, availability -- all filtering is client-side after a single PROPFIND +- **List/grid view**: Cards showing name, location, capacity, availability status +- **Search**: Real-time search across name, description, location +- **Quick book**: Click to start creating an event with the resource + +### Resource Picker (Event Modal) + +When creating/editing an event: +- "Add Room" / "Add Resource" button in the attendees section +- Opens a filtered dropdown/modal showing available resources +- Shows real-time availability for the selected event time +- Displays capacity and key metadata inline +- Selected resources appear in the attendees list with a room/resource icon + +### Resource Timeline + +A horizontal timeline showing: +- One row per resource +- Colored blocks for existing bookings +- Grey blocks for unavailable hours +- Ability to click an empty slot to book + +### Resource Admin Panel + +For resource administrators: +- Create/edit resource metadata (PROPPATCH to CalDAV) +- Configure auto-schedule mode and booking policies (PROPPATCH) +- Manage access via CalDAV sharing (`CS:share`) -- same UI as calendar sharing +- View booking history and usage stats +- Override booking decisions (accept/decline pending requests) + +--- + +## Migration and Deployment + +### Rolling Deployment Steps + +1. **Database migration**: Add `calendar_user_type` to SabreDAV principals (no Django migration needed) +2. **Deploy SabreDAV**: Updated principal backend + auto-schedule plugin (no user impact -- new code paths only activate for resource principals) +3. **Deploy Django backend**: Resource provisioning + access endpoints (additive, no breaking changes) +4. **Deploy frontend**: Resource UI (feature-flagged if needed) + +### Feature Flag + +Consider a `RESOURCES_ENABLED` feature flag in settings to: +- Show/hide resource UI in the frontend +- Enable/disable resource API endpoints +- Allow gradual rollout + +### Data Migration + +If importing resources from an external system: +1. Call the Django provisioning endpoint for each resource (creates CalDAV principals) +2. PROPPATCH to set metadata properties on each resource principal +3. Import historical bookings as calendar events via ICS import +4. Verify free/busy accuracy after import diff --git a/env.d/development/backend.defaults b/env.d/development/backend.defaults index 7141512..bbcf2b8 100644 --- a/env.d/development/backend.defaults +++ b/env.d/development/backend.defaults @@ -17,11 +17,11 @@ PYTHONPATH=/app # Media STORAGES_STATICFILES_BACKEND=django.contrib.staticfiles.storage.StaticFilesStorage -MEDIA_BASE_URL=http://localhost:8923 +MEDIA_BASE_URL=http://localhost:8933 -# OIDC - Keycloak on dedicated port 8925 +# OIDC - Keycloak on dedicated port 8935 OIDC_OP_JWKS_ENDPOINT=http://keycloak:8080/realms/calendars/protocol/openid-connect/certs -OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8925/realms/calendars/protocol/openid-connect/auth +OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8935/realms/calendars/protocol/openid-connect/auth OIDC_OP_TOKEN_ENDPOINT=http://keycloak:8080/realms/calendars/protocol/openid-connect/token OIDC_OP_USER_ENDPOINT=http://keycloak:8080/realms/calendars/protocol/openid-connect/userinfo @@ -30,15 +30,15 @@ OIDC_RP_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly OIDC_RP_SIGN_ALGO=RS256 OIDC_RP_SCOPES="openid email" -LOGIN_REDIRECT_URL=http://localhost:8920 -LOGIN_REDIRECT_URL_FAILURE=http://localhost:8920 -LOGOUT_REDIRECT_URL=http://localhost:8920 +LOGIN_REDIRECT_URL=http://localhost:8930 +LOGIN_REDIRECT_URL_FAILURE=http://localhost:8930 +LOGOUT_REDIRECT_URL=http://localhost:8930 -OIDC_REDIRECT_ALLOWED_HOSTS="http://localhost:8923,http://localhost:8920" +OIDC_REDIRECT_ALLOWED_HOSTS="http://localhost:8933,http://localhost:8930" OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"} # Resource Server Backend -OIDC_OP_URL=http://localhost:8925/realms/calendars +OIDC_OP_URL=http://localhost:8935/realms/calendars OIDC_OP_INTROSPECTION_ENDPOINT=http://keycloak:8080/realms/calendars/protocol/openid-connect/token/introspect OIDC_RESOURCE_SERVER_ENABLED=False OIDC_RS_CLIENT_ID=calendars @@ -50,6 +50,7 @@ OIDC_RS_ALLOWED_AUDIENCES="" CALDAV_URL=http://caldav:80 CALDAV_OUTBOUND_API_KEY=changeme-outbound-in-production CALDAV_INBOUND_API_KEY=changeme-inbound-in-production +CALDAV_INTERNAL_API_KEY=changeme-internal-in-production # Internal URL for CalDAV scheduling callbacks (accessible from CalDAV container) CALDAV_CALLBACK_BASE_URL=http://backend-dev:8000 diff --git a/env.d/development/caldav.defaults b/env.d/development/caldav.defaults index 8ceb8c0..8da2b93 100644 --- a/env.d/development/caldav.defaults +++ b/env.d/development/caldav.defaults @@ -3,9 +3,10 @@ PGPORT=5432 PGDATABASE=calendars PGUSER=pgroot PGPASSWORD=pass -CALDAV_BASE_URI=/api/v1.0/caldav/ +CALDAV_BASE_URI=/caldav/ CALDAV_INBOUND_API_KEY=changeme-inbound-in-production CALDAV_OUTBOUND_API_KEY=changeme-outbound-in-production +CALDAV_INTERNAL_API_KEY=changeme-internal-in-production # Default callback URL for sending scheduling notifications (emails) # Used when clients (like Apple Calendar) don't provide X-CalDAV-Callback-URL header CALDAV_CALLBACK_URL=http://backend-dev:8000/api/v1.0/caldav-scheduling-callback diff --git a/env.d/development/frontend.defaults b/env.d/development/frontend.defaults index 29ee99e..9d3bff9 100644 --- a/env.d/development/frontend.defaults +++ b/env.d/development/frontend.defaults @@ -1,2 +1,2 @@ -NEXT_PUBLIC_API_ORIGIN=http://localhost:8921 +NEXT_PUBLIC_API_ORIGIN=http://localhost:8931 NEXT_TELEMETRY_DISABLED=1 \ No newline at end of file diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index 66b8ee9..8e05c96 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -94,9 +94,6 @@ WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" -# Generate compiled translation messages -RUN DJANGO_CONFIGURATION=Build \ - python manage.py compilemessages --ignore=".venv/**/*" # We wrap commands run in this container by the following entrypoint that diff --git a/src/backend/calendars/__init__.py b/src/backend/calendars/__init__.py index 9cca34c..751101c 100644 --- a/src/backend/calendars/__init__.py +++ b/src/backend/calendars/__init__.py @@ -1,5 +1 @@ -"""Calendars package. Import the celery app early to load shared task form dependencies.""" - -from .celery_app import app as celery_app - -__all__ = ["celery_app"] +"""Calendars Django project package.""" diff --git a/src/backend/calendars/celery_app.py b/src/backend/calendars/celery_app.py deleted file mode 100644 index 7b0f82b..0000000 --- a/src/backend/calendars/celery_app.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Calendars celery configuration file.""" - -import os - -from celery import Celery -from configurations.importer import install - -# Set the default Django settings module for the 'celery' program. -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "calendars.settings") -os.environ.setdefault("DJANGO_CONFIGURATION", "Development") - -install(check_options=True) - -# Can be loaded only after install call. -from django.conf import settings # pylint: disable=wrong-import-position - -app = Celery("calendars") - -# Using a string here means the worker doesn't have to serialize -# the configuration object to child processes. -# - namespace='CELERY' means all celery-related configuration keys -# should have a `CELERY_` prefix. -app.config_from_object("django.conf:settings", namespace="CELERY") - -# Load task modules from all registered Django apps. -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/src/backend/calendars/settings.py b/src/backend/calendars/settings.py index 0eebf70..7cdf115 100755 --- a/src/backend/calendars/settings.py +++ b/src/backend/calendars/settings.py @@ -14,8 +14,6 @@ import os import tomllib from socket import gethostbyname, gethostname -from django.utils.translation import gettext_lazy as _ - import dj_database_url import sentry_sdk from configurations import Configuration, values @@ -74,13 +72,24 @@ class Base(Configuration): # CalDAV API keys for bidirectional authentication # INBOUND: API key for authenticating requests FROM CalDAV server TO Django - CALDAV_INBOUND_API_KEY = values.Value( + CALDAV_INBOUND_API_KEY = SecretFileValue( None, environ_name="CALDAV_INBOUND_API_KEY", environ_prefix=None ) # OUTBOUND: API key for authenticating requests FROM Django TO CalDAV server - CALDAV_OUTBOUND_API_KEY = values.Value( + CALDAV_OUTBOUND_API_KEY = SecretFileValue( None, environ_name="CALDAV_OUTBOUND_API_KEY", environ_prefix=None ) + # INTERNAL: API key for Django → CalDAV internal API (resource provisioning, import) + CALDAV_INTERNAL_API_KEY = SecretFileValue( + None, environ_name="CALDAV_INTERNAL_API_KEY", environ_prefix=None + ) + # Salt for django-fernet-encrypted-fields (Channel tokens, etc.) + # Used with SECRET_KEY to derive Fernet encryption keys via PBKDF2 + SALT_KEY = values.Value( + "calendars-default-salt-change-in-production", + environ_name="SALT_KEY", + environ_prefix=None, + ) # Base URL for CalDAV scheduling callbacks (must be accessible from CalDAV container) # In Docker environments, use the internal Docker network URL (e.g., http://backend:8000) CALDAV_CALLBACK_BASE_URL = values.Value( @@ -117,7 +126,7 @@ class Base(Configuration): CALENDAR_INVITATION_FROM_EMAIL = values.Value( None, environ_name="CALENDAR_INVITATION_FROM_EMAIL", environ_prefix=None ) - APP_NAME = values.Value("Calendrier", environ_name="APP_NAME", environ_prefix=None) + APP_NAME = values.Value("Calendars", environ_name="APP_NAME", environ_prefix=None) APP_URL = values.Value("", environ_name="APP_URL", environ_prefix=None) CALENDAR_ITIP_ENABLED = values.BooleanValue( False, environ_name="CALENDAR_ITIP_ENABLED", environ_prefix=None @@ -133,6 +142,18 @@ class Base(Configuration): environ_prefix=None, ) + # Organizations + OIDC_USERINFO_ORGANIZATION_CLAIM = values.Value( + "", + environ_name="OIDC_USERINFO_ORGANIZATION_CLAIM", + environ_prefix=None, + ) + RESOURCE_EMAIL_DOMAIN = values.Value( + "", + environ_name="RESOURCE_EMAIL_DOMAIN", + environ_prefix=None, + ) + # Entitlements ENTITLEMENTS_BACKEND = values.Value( "core.entitlements.backends.local.LocalEntitlementsBackend", @@ -212,7 +233,7 @@ class Base(Configuration): # This is used to limit the size of the request body in memory. # This also limits the size of the file that can be uploaded to the server. DATA_UPLOAD_MAX_MEMORY_SIZE = values.PositiveIntegerValue( - 2 * (2**30), # 2GB + 20 * (2**20), # 20MB environ_name="DATA_UPLOAD_MAX_MEMORY_SIZE", environ_prefix=None, ) @@ -234,15 +255,13 @@ class Base(Configuration): # fallback/default languages throughout the app. LANGUAGES = values.SingleNestedTupleValue( ( - ("en-us", _("English")), - ("fr-fr", _("French")), - ("de-de", _("German")), - ("nl-nl", _("Dutch")), + ("en-us", "English"), + ("fr-fr", "French"), + ("de-de", "German"), + ("nl-nl", "Dutch"), ) ) - LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),) - TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True @@ -275,7 +294,6 @@ class Base(Configuration): "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.locale.LocaleMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", @@ -296,7 +314,7 @@ class Base(Configuration): "drf_standardized_errors", # Third party apps "corsheaders", - "django_celery_beat", + "django_dramatiq", "django_filters", "rest_framework", "rest_framework_api_key", @@ -415,7 +433,11 @@ class Base(Configuration): ) AUTH_USER_MODEL = "core.User" - INVITATION_VALIDITY_DURATION = 604800 # 7 days, in seconds + RSVP_TOKEN_MAX_AGE_RECURRING = values.PositiveIntegerValue( + 7776000, # 90 days + environ_name="RSVP_TOKEN_MAX_AGE_RECURRING", + environ_prefix=None, + ) # CORS CORS_ALLOW_CREDENTIALS = True @@ -514,10 +536,40 @@ class Base(Configuration): THUMBNAIL_DEFAULT_STORAGE_ALIAS = "default" THUMBNAIL_ALIASES = {} - # Celery - CELERY_BROKER_URL = values.Value("redis://redis:6379/0") - CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({}) - CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" + # Dramatiq + DRAMATIQ_BROKER = { + "BROKER": "dramatiq.brokers.redis.RedisBroker", + "OPTIONS": { + "url": values.Value( + "redis://redis:6379/0", + environ_name="DRAMATIQ_BROKER_URL", + environ_prefix=None, + ), + }, + "MIDDLEWARE": [ + "dramatiq.middleware.AgeLimit", + "dramatiq.middleware.TimeLimit", + "dramatiq.middleware.Callbacks", + "dramatiq.middleware.Retries", + "dramatiq.middleware.CurrentMessage", + "django_dramatiq.middleware.DbConnectionsMiddleware", + "django_dramatiq.middleware.AdminMiddleware", + ], + } + DRAMATIQ_RESULT_BACKEND = { + "BACKEND": "dramatiq.results.backends.redis.RedisBackend", + "BACKEND_OPTIONS": { + "url": values.Value( + "redis://redis:6379/1", + environ_name="DRAMATIQ_RESULT_BACKEND_URL", + environ_prefix=None, + ), + }, + "MIDDLEWARE_OPTIONS": { + "result_ttl": 1000 * 60 * 60 * 24 * 30, # 30 days + }, + } + DRAMATIQ_AUTODISCOVER_MODULES = ["tasks"] # Session SESSION_ENGINE = "django.contrib.sessions.backends.cache" @@ -635,12 +687,6 @@ class Base(Configuration): environ_name="OIDC_USERINFO_FULLNAME_FIELDS", environ_prefix=None, ) - OIDC_USERINFO_SHORTNAME_FIELD = values.Value( - default="first_name", - environ_name="OIDC_USERINFO_SHORTNAME_FIELD", - environ_prefix=None, - ) - # OIDC Resource Server OIDC_RESOURCE_SERVER_ENABLED = values.BooleanValue( @@ -870,7 +916,7 @@ class Development(Base): ALLOWED_HOSTS = ["*"] CORS_ALLOW_ALL_ORIGINS = True CSRF_TRUSTED_ORIGINS = [ - "http://localhost:8920", + "http://localhost:8930", "http://localhost:3000", ] DEBUG = True @@ -887,8 +933,8 @@ class Development(Base): EMAIL_USE_SSL = False DEFAULT_FROM_EMAIL = "calendars@calendars.world" CALENDAR_INVITATION_FROM_EMAIL = "calendars@calendars.world" - APP_NAME = "Calendrier (Dev)" - APP_URL = "http://localhost:8921" + APP_NAME = "Calendars (dev)" + APP_URL = "http://localhost:8931" DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": lambda request: True, @@ -919,7 +965,18 @@ class Test(Base): ] USE_SWAGGER = True - CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True) + DRAMATIQ_BROKER = { + "BROKER": "core.task_utils.EagerBroker", + "OPTIONS": {}, + "MIDDLEWARE": [ + "dramatiq.middleware.CurrentMessage", + ], + } + DRAMATIQ_RESULT_BACKEND = { + "BACKEND": "dramatiq.results.backends.stub.StubBackend", + "BACKEND_OPTIONS": {}, + "MIDDLEWARE_OPTIONS": {"result_ttl": 1000 * 60 * 10}, + } OIDC_STORE_ACCESS_TOKEN = False OIDC_STORE_REFRESH_TOKEN = False @@ -977,6 +1034,7 @@ class Production(Base): "^__lbheartbeat__", "^__heartbeat__", r"^api/v1\.0/caldav-scheduling-callback/", + r"^caldav/", ] # Modern browsers require to have the `secure` attribute on cookies with `Samesite=none` diff --git a/src/backend/core/admin.py b/src/backend/core/admin.py index 8fdb2dd..f0525fa 100644 --- a/src/backend/core/admin.py +++ b/src/backend/core/admin.py @@ -2,7 +2,6 @@ from django.contrib import admin from django.contrib.auth import admin as auth_admin -from django.utils.translation import gettext_lazy as _ from . import models @@ -23,20 +22,19 @@ class UserAdmin(auth_admin.UserAdmin): }, ), ( - _("Personal info"), + "Personal info", { "fields": ( "sub", "email", "full_name", - "short_name", "language", "timezone", ) }, ), ( - _("Permissions"), + "Permissions", { "fields": ( "is_active", @@ -48,7 +46,7 @@ class UserAdmin(auth_admin.UserAdmin): ), }, ), - (_("Important dates"), {"fields": ("created_at", "updated_at")}), + ("Important dates", {"fields": ("created_at", "updated_at")}), ) add_fieldsets = ( ( @@ -86,27 +84,27 @@ class UserAdmin(auth_admin.UserAdmin): "sub", "email", "full_name", - "short_name", "created_at", "updated_at", ) search_fields = ("id", "sub", "admin_email", "email", "full_name") -@admin.register(models.CalendarSubscriptionToken) -class CalendarSubscriptionTokenAdmin(admin.ModelAdmin): - """Admin class for CalendarSubscriptionToken model.""" +@admin.register(models.Channel) +class ChannelAdmin(admin.ModelAdmin): + """Admin class for Channel model.""" list_display = ( - "calendar_name", - "owner", + "name", + "type", + "organization", + "user", "caldav_path", - "token", "is_active", - "last_accessed_at", + "last_used_at", "created_at", ) - list_filter = ("is_active",) - search_fields = ("calendar_name", "owner__email", "caldav_path", "token") - readonly_fields = ("id", "token", "created_at", "last_accessed_at") - raw_id_fields = ("owner",) + list_filter = ("type", "is_active") + search_fields = ("name", "user__email", "caldav_path") + readonly_fields = ("id", "created_at", "updated_at", "last_used_at") + raw_id_fields = ("user", "organization") diff --git a/src/backend/core/api/permissions.py b/src/backend/core/api/permissions.py index de57cda..ce19856 100644 --- a/src/backend/core/api/permissions.py +++ b/src/backend/core/api/permissions.py @@ -2,19 +2,12 @@ import logging -from django.core import exceptions - from rest_framework import permissions from core.entitlements import EntitlementsUnavailableError, get_user_entitlements logger = logging.getLogger(__name__) -ACTION_FOR_METHOD_TO_PERMISSION = { - "versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"}, - "children": {"GET": "children_list", "POST": "children_create"}, -} - class IsAuthenticated(permissions.BasePermission): """ @@ -26,15 +19,6 @@ class IsAuthenticated(permissions.BasePermission): return bool(request.auth) or request.user.is_authenticated -class IsAuthenticatedOrSafe(IsAuthenticated): - """Allows access to authenticated users (or anonymous users but only on safe methods).""" - - def has_permission(self, request, view): - if request.method in permissions.SAFE_METHODS: - return True - return super().has_permission(request, view) - - class IsSelf(IsAuthenticated): """ Allows access only to authenticated users. Alternative method checking the presence @@ -46,27 +30,7 @@ class IsSelf(IsAuthenticated): return obj == request.user -class IsOwnedOrPublic(IsAuthenticated): - """ - Allows access to authenticated users only for objects that are owned or not related - to any user via the "owner" field. - """ - - def has_object_permission(self, request, view, obj): - """Unsafe permissions are only allowed for the owner of the object.""" - if obj.owner == request.user: - return True - - if request.method in permissions.SAFE_METHODS and obj.owner is None: - return True - - try: - return obj.user == request.user - except exceptions.ObjectDoesNotExist: - return False - - -class IsEntitled(IsAuthenticated): +class IsEntitledToAccess(IsAuthenticated): """Allows access only to users with can_access entitlement. Fail-closed: denies access when the entitlements service is @@ -78,25 +42,31 @@ class IsEntitled(IsAuthenticated): return False try: entitlements = get_user_entitlements(request.user.sub, request.user.email) - return entitlements.get("can_access", True) + return entitlements.get("can_access", False) except EntitlementsUnavailableError: + logger.warning( + "Entitlements unavailable, denying access for user %s", + request.user.pk, + ) return False -class AccessPermission(permissions.BasePermission): - """Permission class for access objects.""" +class IsOrgAdmin(IsAuthenticated): + """Allows access only to users with can_admin entitlement. + + Fail-closed: denies access when the entitlements service is + unavailable and no cached value exists. + """ def has_permission(self, request, view): - return request.user.is_authenticated or view.action not in [ - "create", - ] - - def has_object_permission(self, request, view, obj): - """Check permission for a given object.""" - abilities = obj.get_abilities(request.user) - action = view.action + if not super().has_permission(request, view): + return False try: - action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method] - except KeyError: - pass - return abilities.get(action, False) + entitlements = get_user_entitlements(request.user.sub, request.user.email) + return entitlements.get("can_admin", False) + except EntitlementsUnavailableError: + logger.warning( + "Entitlements unavailable, denying admin for user %s", + request.user.pk, + ) + return False diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 8575d00..9f81108 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -1,13 +1,22 @@ """Client serializers for the calendars core app.""" from django.conf import settings -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ +from django.utils.text import slugify -from rest_framework import exceptions, serializers +from rest_framework import serializers from core import models from core.entitlements import EntitlementsUnavailableError, get_user_entitlements +from core.models import uuid_to_urlsafe + + +class OrganizationSerializer(serializers.ModelSerializer): + """Serialize organizations.""" + + class Meta: + model = models.Organization + fields = ["id", "name"] + read_only_fields = ["id", "name"] class UserLiteSerializer(serializers.ModelSerializer): @@ -15,171 +24,174 @@ class UserLiteSerializer(serializers.ModelSerializer): class Meta: model = models.User - fields = ["id", "full_name", "short_name"] - read_only_fields = ["id", "full_name", "short_name"] - - -class BaseAccessSerializer(serializers.ModelSerializer): - """Serialize template accesses.""" - - abilities = serializers.SerializerMethodField(read_only=True) - - def update(self, instance, validated_data): - """Make "user" field is readonly but only on update.""" - validated_data.pop("user", None) - return super().update(instance, validated_data) - - def get_abilities(self, access) -> dict: - """Return abilities of the logged-in user on the instance.""" - request = self.context.get("request") - if request: - return access.get_abilities(request.user) - return {} - - def validate(self, attrs): - """ - Check access rights specific to writing (create/update) - """ - request = self.context.get("request") - user = getattr(request, "user", None) - role = attrs.get("role") - - # Update - if self.instance: - can_set_role_to = self.instance.get_abilities(user)["set_role_to"] - - if role and role not in can_set_role_to: - message = ( - f"You are only allowed to set role to {', '.join(can_set_role_to)}" - if can_set_role_to - else "You are not allowed to set this role for this template." - ) - raise exceptions.PermissionDenied(message) - - # Create - else: - try: - resource_id = self.context["resource_id"] - except KeyError as exc: - raise exceptions.ValidationError( - "You must set a resource ID in kwargs to create a new access." - ) from exc - - if not self.Meta.model.objects.filter( # pylint: disable=no-member - Q(user=user) | Q(team__in=user.teams), - role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN], - **{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member - ).exists(): - raise exceptions.PermissionDenied( - "You are not allowed to manage accesses for this resource." - ) - - if ( - role == models.RoleChoices.OWNER - and not self.Meta.model.objects.filter( # pylint: disable=no-member - Q(user=user) | Q(team__in=user.teams), - role=models.RoleChoices.OWNER, - **{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member - ).exists() - ): - raise exceptions.PermissionDenied( - "Only owners of a resource can assign other users as owners." - ) - - # pylint: disable=no-member - attrs[f"{self.Meta.resource_field_name}_id"] = self.context["resource_id"] - return attrs + fields = ["id", "full_name"] + read_only_fields = ["id", "full_name"] class UserSerializer(serializers.ModelSerializer): """Serialize users.""" + email = serializers.SerializerMethodField(read_only=True) + class Meta: model = models.User fields = [ "id", "email", "full_name", - "short_name", "language", ] - read_only_fields = ["id", "email", "full_name", "short_name"] + read_only_fields = ["id", "email", "full_name"] + + def get_email(self, user) -> str | None: + """Return OIDC email, falling back to admin_email for staff users.""" + return user.email or user.admin_email class UserMeSerializer(UserSerializer): """Serialize users for me endpoint.""" can_access = serializers.SerializerMethodField(read_only=True) + can_admin = serializers.SerializerMethodField(read_only=True) + organization = OrganizationSerializer(read_only=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._entitlements_cache = {} class Meta: model = models.User - fields = [*UserSerializer.Meta.fields, "can_access"] - read_only_fields = [*UserSerializer.Meta.read_only_fields, "can_access"] + fields = [ + *UserSerializer.Meta.fields, + "can_access", + "can_admin", + "organization", + ] + read_only_fields = [ + *UserSerializer.Meta.read_only_fields, + "can_access", + "can_admin", + "organization", + ] + + def _get_entitlements(self, user): + """Get cached entitlements for the user, keyed by user.sub. + + Cache is per-serializer-instance (request-scoped) to avoid + duplicate calls when both can_access and can_admin are serialized. + """ + if user.sub not in self._entitlements_cache: + try: + self._entitlements_cache[user.sub] = get_user_entitlements( + user.sub, user.email + ) + except EntitlementsUnavailableError: + self._entitlements_cache[user.sub] = None + return self._entitlements_cache[user.sub] def get_can_access(self, user) -> bool: """Check entitlements for the current user.""" - try: - entitlements = get_user_entitlements(user.sub, user.email) - return entitlements.get("can_access", True) - except EntitlementsUnavailableError: - return True # fail-open + entitlements = self._get_entitlements(user) + if entitlements is None: + return False # fail-closed + return entitlements.get("can_access", False) + + def get_can_admin(self, user) -> bool: + """Check admin entitlement for the current user.""" + entitlements = self._get_entitlements(user) + if entitlements is None: + return False # fail-closed + return entitlements.get("can_admin", False) -class CalendarSubscriptionTokenSerializer(serializers.ModelSerializer): - """Serializer for CalendarSubscriptionToken model.""" +class ChannelSerializer(serializers.ModelSerializer): + """Read serializer for Channel model.""" + role = serializers.SerializerMethodField() url = serializers.SerializerMethodField() class Meta: - model = models.CalendarSubscriptionToken + model = models.Channel fields = [ - "token", - "url", + "id", + "name", + "type", + "organization", + "user", "caldav_path", - "calendar_name", + "role", "is_active", - "last_accessed_at", - "created_at", - ] - read_only_fields = [ - "token", + "settings", "url", - "caldav_path", - "calendar_name", - "is_active", - "last_accessed_at", + "last_used_at", "created_at", + "updated_at", ] + read_only_fields = fields + + def get_role(self, obj): + """Get role from settings.""" + return obj.role + + def get_url(self, obj) -> str | None: + """Build iCal subscription URL for ical-feed channels, None otherwise.""" + if obj.type != "ical-feed": + return None + + token = obj.encrypted_settings.get("token", "") + if not token: + return None + + short_id = uuid_to_urlsafe(obj.pk) + calendar_name = obj.settings.get("calendar_name", "") + filename = slugify(calendar_name)[:50] or "feed" + ical_path = f"/ical/{short_id}/{token}/{filename}.ics" - def get_url(self, obj) -> str: - """Build the full subscription URL, enforcing HTTPS in production.""" request = self.context.get("request") if request: - url = request.build_absolute_uri(f"/ical/{obj.token}.ics") + url = request.build_absolute_uri(ical_path) else: - # Fallback to APP_URL if no request context - app_url = getattr(settings, "APP_URL", "") - url = f"{app_url.rstrip('/')}/ical/{obj.token}.ics" + app_url = settings.APP_URL + url = f"{app_url.rstrip('/')}{ical_path}" - # Force HTTPS in production to protect the token in transit if not settings.DEBUG and url.startswith("http://"): url = url.replace("http://", "https://", 1) return url -class CalendarSubscriptionTokenCreateSerializer(serializers.Serializer): # pylint: disable=abstract-method - """Serializer for creating a CalendarSubscriptionToken.""" +class ChannelCreateSerializer(serializers.Serializer): # pylint: disable=abstract-method + """Write serializer for creating a Channel.""" - caldav_path = serializers.CharField(max_length=512) + name = serializers.CharField(max_length=255) + type = serializers.CharField(max_length=255, default="caldav") + caldav_path = serializers.CharField(max_length=512, required=False, default="") calendar_name = serializers.CharField(max_length=255, required=False, default="") + role = serializers.ChoiceField( + choices=[(r, r) for r in models.Channel.VALID_ROLES], + default=models.Channel.ROLE_READER, + ) def validate_caldav_path(self, value): - """Validate and normalize the caldav_path.""" - # Normalize path to always have trailing slash - if not value.endswith("/"): - value = value + "/" - # Normalize path to always start with / - if not value.startswith("/"): - value = "/" + value + """Normalize caldav_path if provided.""" + if value: + if not value.endswith("/"): + value = value + "/" + if not value.startswith("/"): + value = "/" + value return value + + def validate_type(self, value): + """Validate channel type.""" + if value == "ical-feed": + return value + return "caldav" + + +class ChannelWithTokenSerializer(ChannelSerializer): + """Serializer that includes the plaintext token (used only on creation).""" + + token = serializers.CharField(read_only=True) + + class Meta(ChannelSerializer.Meta): + fields = [*ChannelSerializer.Meta.fields, "token"] diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index a62d679..ae519f5 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1,5 +1,4 @@ """API endpoints""" -# pylint: disable=too-many-lines import json import logging @@ -8,9 +7,7 @@ from django.conf import settings from django.core.cache import cache from django.utils.text import slugify -import rest_framework as drf -from rest_framework import response as drf_response -from rest_framework import status, viewsets +from rest_framework import mixins, pagination, response, status, views, viewsets from rest_framework.decorators import action from rest_framework.parsers import MultiPartParser from rest_framework.permissions import AllowAny, IsAuthenticated @@ -21,7 +18,8 @@ from core.services.caldav_service import ( normalize_caldav_path, verify_caldav_access, ) -from core.services.import_service import MAX_FILE_SIZE, ICSImportService +from core.services.import_service import MAX_FILE_SIZE +from core.services.resource_service import ResourceProvisioningError, ResourceService from . import permissions, serializers @@ -31,60 +29,6 @@ logger = logging.getLogger(__name__) # pylint: disable=too-many-ancestors -class NestedGenericViewSet(viewsets.GenericViewSet): - """ - A generic Viewset aims to be used in a nested route context. - e.g: `/api/v1.0/resource_1//resource_2//` - - It allows to define all url kwargs and lookup fields to perform the lookup. - """ - - lookup_fields: list[str] = ["pk"] - lookup_url_kwargs: list[str] = [] - - def __getattribute__(self, item): - """ - This method is overridden to allow to get the last lookup field or lookup url kwarg - when accessing the `lookup_field` or `lookup_url_kwarg` attribute. This is useful - to keep compatibility with all methods used by the parent class `GenericViewSet`. - """ - if item in ["lookup_field", "lookup_url_kwarg"]: - return getattr(self, item + "s", [None])[-1] - - return super().__getattribute__(item) - - def get_queryset(self): - """ - Get the list of items for this view. - - `lookup_fields` attribute is enumerated here to perform the nested lookup. - """ - queryset = super().get_queryset() - - # The last lookup field is removed to perform the nested lookup as it corresponds - # to the object pk, it is used within get_object method. - lookup_url_kwargs = ( - self.lookup_url_kwargs[:-1] - if self.lookup_url_kwargs - else self.lookup_fields[:-1] - ) - - filter_kwargs = {} - for index, lookup_url_kwarg in enumerate(lookup_url_kwargs): - if lookup_url_kwarg not in self.kwargs: - raise KeyError( - f"Expected view {self.__class__.__name__} to be called with a URL " - f'keyword argument named "{lookup_url_kwarg}". Fix your URL conf, or ' - "set the `.lookup_fields` attribute on the view correctly." - ) - - filter_kwargs.update( - {self.lookup_fields[index]: self.kwargs[lookup_url_kwarg]} - ) - - return queryset.filter(**filter_kwargs) - - class SerializerPerActionMixin: """ A mixin to allow to define serializer classes for each action. @@ -110,10 +54,10 @@ class SerializerPerActionMixin: return super().get_serializer_class() -class Pagination(drf.pagination.PageNumberPagination): +class Pagination(pagination.PageNumberPagination): """Pagination to display no more than 100 objects per page sorted by creation date.""" - ordering = "-created_on" + ordering = "-created_at" max_page_size = settings.MAX_PAGE_SIZE page_size_query_param = "page_size" @@ -132,9 +76,9 @@ class UserListThrottleSustained(UserRateThrottle): class UserViewSet( SerializerPerActionMixin, - drf.mixins.UpdateModelMixin, + mixins.UpdateModelMixin, viewsets.GenericViewSet, - drf.mixins.ListModelMixin, + mixins.ListModelMixin, ): """User ViewSet""" @@ -142,7 +86,7 @@ class UserViewSet( queryset = models.User.objects.all().filter(is_active=True) serializer_class = serializers.UserSerializer get_me_serializer_class = serializers.UserMeSerializer - pagination_class = None + pagination_class = Pagination throttle_classes = [] def get_throttles(self): @@ -155,6 +99,7 @@ class UserViewSet( def get_queryset(self): """ Limit listed users by querying the email field. + Scoped to the requesting user's organization. If query contains "@", search exactly. Otherwise return empty. """ queryset = self.queryset @@ -162,19 +107,22 @@ class UserViewSet( if self.action != "list": return queryset + # Scope to same organization; users without an org see no results + if not self.request.user.organization_id: + return queryset.none() + queryset = queryset.filter(organization_id=self.request.user.organization_id) + if not (query := self.request.query_params.get("q", "")) or len(query) < 5: return queryset.none() # For emails, match exactly if "@" in query: - return queryset.filter(email__iexact=query).order_by("email")[ - : settings.API_USERS_LIST_LIMIT - ] + return queryset.filter(email__iexact=query).order_by("email") # For non-email queries, return empty (no fuzzy search) return queryset.none() - @drf.decorators.action( + @action( detail=False, methods=["get"], url_name="me", @@ -185,12 +133,12 @@ class UserViewSet( Return information on currently logged user """ context = {"request": request} - return drf.response.Response( + return response.Response( self.get_serializer(request.user, context=context).data ) -class ConfigView(drf.views.APIView): +class ConfigView(views.APIView): """API ViewSet for sharing some public settings.""" permission_classes = [AllowAny] @@ -224,7 +172,7 @@ class ConfigView(drf.views.APIView): dict_settings["theme_customization"] = self._load_theme_customization() - return drf.response.Response(dict_settings) + return response.Response(dict_settings) def _load_theme_customization(self): if not settings.THEME_CUSTOMIZATION_FILE_PATH: @@ -272,7 +220,7 @@ class CalendarViewSet(viewsets.GenericViewSet): def get_permissions(self): if self.action == "import_events": - return [permissions.IsEntitled()] + return [permissions.IsEntitledToAccess()] return super().get_permissions() @action( @@ -286,12 +234,18 @@ class CalendarViewSet(viewsets.GenericViewSet): """Import events from an ICS file into a calendar. POST /api/v1.0/calendars/import-events/ - Body (multipart): file=, caldav_path=/calendars/user@.../uuid/ + Body (multipart): file=, caldav_path=/calendars/users/user@.../uuid/ + + Returns a task_id that can be polled at GET /api/v1.0/tasks/{task_id}/ """ + from core.tasks import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + import_events_task, + ) + caldav_path = request.data.get("caldav_path", "") if not caldav_path: - return drf_response.Response( - {"error": "caldav_path is required"}, + return response.Response( + {"detail": "caldav_path is required"}, status=status.HTTP_400_BAD_REQUEST, ) @@ -299,15 +253,15 @@ class CalendarViewSet(viewsets.GenericViewSet): # Verify user access if not verify_caldav_access(request.user, caldav_path): - return drf_response.Response( - {"error": "You don't have access to this calendar"}, + return response.Response( + {"detail": "You don't have access to this calendar"}, status=status.HTTP_403_FORBIDDEN, ) # Validate file presence if "file" not in request.FILES: - return drf_response.Response( - {"error": "No file provided"}, + return response.Response( + {"detail": "No file provided"}, status=status.HTTP_400_BAD_REQUEST, ) @@ -315,124 +269,81 @@ class CalendarViewSet(viewsets.GenericViewSet): # Validate file size if uploaded_file.size > MAX_FILE_SIZE: - return drf_response.Response( - {"error": "File too large. Maximum size is 10 MB."}, + return response.Response( + {"detail": "File too large. Maximum size is 10 MB."}, status=status.HTTP_400_BAD_REQUEST, ) ics_data = uploaded_file.read() - service = ICSImportService() - result = service.import_events(request.user, caldav_path, ics_data) - response_data = { - "total_events": result.total_events, - "imported_count": result.imported_count, - "duplicate_count": result.duplicate_count, - "skipped_count": result.skipped_count, - } - if result.errors: - response_data["errors"] = result.errors + # Queue the import task + task = import_events_task.delay( + str(request.user.id), + caldav_path, + ics_data.hex(), + ) + task.track_owner(request.user.id) - return drf_response.Response(response_data, status=status.HTTP_200_OK) + return response.Response( + {"task_id": task.id}, + status=status.HTTP_202_ACCEPTED, + ) -class SubscriptionTokenViewSet(viewsets.GenericViewSet): - """ - ViewSet for managing subscription tokens independently of Django Calendar model. +class ResourceViewSet(viewsets.ViewSet): + """ViewSet for resource provisioning (create/delete). - This viewset operates directly with CalDAV paths, without requiring a Django - Calendar record. The backend verifies that the user has access to the calendar - by checking that their email is in the CalDAV path. - - Endpoints: - - POST /api/v1.0/subscription-tokens/ - Create or get existing token - - GET /api/v1.0/subscription-tokens/by-path/ - Get token by CalDAV path - - DELETE /api/v1.0/subscription-tokens/by-path/ - Delete token by CalDAV path + Resources are CalDAV principals — this endpoint only handles + provisioning. All metadata, sharing, and discovery goes through CalDAV. """ - permission_classes = [IsAuthenticated] - serializer_class = serializers.CalendarSubscriptionTokenSerializer + permission_classes = [permissions.IsOrgAdmin] def create(self, request): + """Create a resource principal and its default calendar. + + POST /api/v1.0/resources/ + Body: {"name": "Room 101", "resource_type": "ROOM"} """ - Create or get existing subscription token. + name = request.data.get("name", "").strip() + resource_type = request.data.get("resource_type", "ROOM").strip().upper() - POST body: - - caldav_path: The CalDAV path (e.g., /calendars/user@example.com/uuid/) - - calendar_name: Display name of the calendar (optional) - """ - create_serializer = serializers.CalendarSubscriptionTokenCreateSerializer( - data=request.data - ) - create_serializer.is_valid(raise_exception=True) - - caldav_path = create_serializer.validated_data["caldav_path"] - calendar_name = create_serializer.validated_data.get("calendar_name", "") - - # Verify user has access to this calendar - if not verify_caldav_access(request.user, caldav_path): - return drf_response.Response( - {"error": "You don't have access to this calendar"}, - status=status.HTTP_403_FORBIDDEN, - ) - - # Get or create token - token, created = models.CalendarSubscriptionToken.objects.get_or_create( - owner=request.user, - caldav_path=caldav_path, - defaults={"calendar_name": calendar_name}, - ) - - # Update calendar_name if provided and different - if not created and calendar_name and token.calendar_name != calendar_name: - token.calendar_name = calendar_name - token.save(update_fields=["calendar_name"]) - - serializer = self.get_serializer(token, context={"request": request}) - return drf_response.Response( - serializer.data, - status=status.HTTP_201_CREATED if created else status.HTTP_200_OK, - ) - - @action(detail=False, methods=["get", "delete"], url_path="by-path") - def by_path(self, request): - """ - Get or delete subscription token by CalDAV path. - - Query parameter: - - caldav_path: The CalDAV path (e.g., /calendars/user@example.com/uuid/) - """ - caldav_path = request.query_params.get("caldav_path") - if not caldav_path: - return drf_response.Response( - {"error": "caldav_path query parameter is required"}, + if not name: + return response.Response( + {"detail": "name is required."}, status=status.HTTP_400_BAD_REQUEST, ) - caldav_path = normalize_caldav_path(caldav_path) - - # Verify user has access to this calendar - if not verify_caldav_access(request.user, caldav_path): - return drf_response.Response( - {"error": "You don't have access to this calendar"}, - status=status.HTTP_403_FORBIDDEN, - ) - + service = ResourceService() try: - token = models.CalendarSubscriptionToken.objects.get( - owner=request.user, - caldav_path=caldav_path, - ) - except models.CalendarSubscriptionToken.DoesNotExist: - return drf_response.Response( - {"error": "No subscription token exists for this calendar"}, - status=status.HTTP_404_NOT_FOUND, + result = service.create_resource(request.user, name, resource_type) + except ResourceProvisioningError as e: + return response.Response( + {"detail": str(e)}, + status=status.HTTP_400_BAD_REQUEST, ) - if request.method == "GET": - serializer = self.get_serializer(token, context={"request": request}) - return drf_response.Response(serializer.data) + return response.Response(result, status=status.HTTP_201_CREATED) - # DELETE - token.delete() - return drf_response.Response(status=status.HTTP_204_NO_CONTENT) + def destroy(self, request, pk=None): + """Delete a resource principal and its calendar. + + DELETE /api/v1.0/resources/{resource_id}/ + """ + resource_id = pk + if not resource_id: + return response.Response( + {"detail": "Resource ID is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + service = ResourceService() + try: + service.delete_resource(request.user, resource_id) + except ResourceProvisioningError as e: + return response.Response( + {"detail": str(e)}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return response.Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/backend/core/api/viewsets_caldav.py b/src/backend/core/api/viewsets_caldav.py index 4e7bc81..fb80446 100644 --- a/src/backend/core/api/viewsets_caldav.py +++ b/src/backend/core/api/viewsets_caldav.py @@ -1,11 +1,14 @@ """CalDAV proxy views for forwarding requests to CalDAV server.""" import logging +import re import secrets from django.conf import settings +from django.core.validators import validate_email from django.http import HttpResponse from django.urls import reverse +from django.utils import timezone from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt @@ -13,6 +16,7 @@ from django.views.decorators.csrf import csrf_exempt import requests from core.entitlements import EntitlementsUnavailableError, get_user_entitlements +from core.models import Channel from core.services.caldav_service import CalDAVHTTPClient, validate_caldav_proxy_path from core.services.calendar_invitation_service import calendar_invitation_service @@ -30,6 +34,73 @@ class CalDAVProxyView(View): Authentication is handled via session cookies instead. """ + # HTTP methods allowed per Channel role + READER_METHODS = frozenset({"GET", "PROPFIND", "REPORT", "OPTIONS"}) + EDITOR_METHODS = READER_METHODS | frozenset({"PUT", "POST", "DELETE", "PROPPATCH"}) + ADMIN_METHODS = EDITOR_METHODS | frozenset({"MKCALENDAR", "MKCOL"}) + + ROLE_METHODS = { + Channel.ROLE_READER: READER_METHODS, + Channel.ROLE_EDITOR: EDITOR_METHODS, + Channel.ROLE_ADMIN: ADMIN_METHODS, + } + + @staticmethod + def _authenticate_channel_token(request): + """Try to authenticate via X-Channel-Id + X-Channel-Token headers. + + Returns (channel, user) on success, (None, None) on failure. + """ + channel_id = request.headers.get("X-Channel-Id", "").strip() + token = request.headers.get("X-Channel-Token", "").strip() + if not channel_id or not token: + return None, None + + try: + channel = Channel.objects.get(pk=channel_id, is_active=True, type="caldav") + except (ValueError, Channel.DoesNotExist): + return None, None + + if not channel.verify_token(token): + return None, None + + user = channel.user + if not user: + logger.warning("Channel %s has no user", channel.id) + return None, None + + # Update last_used_at (fire-and-forget, no extra query on critical path) + Channel.objects.filter(pk=channel.pk).update(last_used_at=timezone.now()) + + return channel, user + + @staticmethod + def _check_channel_path_access(channel, path): + """Check that the CalDAV path is within the channel's scope. + + Returns True if allowed, False if denied. + """ + # Ensure path starts with / + full_path = "/" + path.lstrip("/") if path else "/" + + # caldav_path scope: request must be within the scoped calendar + # The trailing slash on caldav_path (enforced by serializer) ensures + # /cal1/ won't match /cal1-secret/ + if channel.caldav_path: + if not channel.caldav_path.endswith("/"): + logger.error( + "caldav_path %r missing trailing slash", channel.caldav_path + ) + return False + return full_path.startswith(channel.caldav_path) + + # user scope: request must be under the user's calendars + if channel.user: + user_prefix = f"/calendars/users/{channel.user.email}/" + return full_path.startswith(user_prefix) + + return False + @staticmethod def _check_entitlements_for_creation(user): """Check if user is entitled to create calendars. @@ -39,7 +110,7 @@ class CalDAVProxyView(View): """ try: entitlements = get_user_entitlements(user.sub, user.email) - if not entitlements.get("can_access", True): + if not entitlements.get("can_access", False): return HttpResponse( status=403, content="Calendar creation not allowed", @@ -51,27 +122,42 @@ class CalDAVProxyView(View): ) return None - def dispatch(self, request, *args, **kwargs): # noqa: PLR0912, PLR0911, PLR0915 # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements + def dispatch(self, request, *args, **kwargs): # noqa: PLR0912, PLR0911, PLR0915 # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements,too-many-locals """Forward all HTTP methods to CalDAV server.""" # Handle CORS preflight requests if request.method == "OPTIONS": response = HttpResponse(status=200) response["Access-Control-Allow-Methods"] = ( - "GET, OPTIONS, PROPFIND, PROPPATCH, REPORT, MKCOL, MKCALENDAR, PUT, DELETE, POST" + "GET, OPTIONS, PROPFIND, PROPPATCH, REPORT," + " MKCOL, MKCALENDAR, PUT, DELETE, POST" ) response["Access-Control-Allow-Headers"] = ( - "Content-Type, depth, authorization, if-match, if-none-match, prefer" + "Content-Type, depth, x-channel-id, x-channel-token," + " if-match, if-none-match, prefer" ) return response + # Try channel token auth first (for external services like Messages) + channel = None + effective_user = None if not request.user.is_authenticated: - return HttpResponse(status=401) + channel, effective_user = self._authenticate_channel_token(request) + if not channel: + return HttpResponse(status=401) + else: + effective_user = request.user - # Block calendar creation (MKCALENDAR/MKCOL) for non-entitled users. - # Other methods (GET, PROPFIND, REPORT, PUT, DELETE, etc.) are allowed - # so that users invited to shared calendars can still use them. + if channel: + # Enforce role-based method restrictions + allowed = self.ROLE_METHODS.get(channel.role, self.READER_METHODS) + if request.method not in allowed: + return HttpResponse( + status=403, content="Method not allowed for this role" + ) + + # Check entitlements for calendar creation (all auth methods) if request.method in ("MKCALENDAR", "MKCOL"): - if denied := self._check_entitlements_for_creation(request.user): + if denied := self._check_entitlements_for_creation(effective_user): return denied # Build the CalDAV server URL @@ -81,8 +167,9 @@ class CalDAVProxyView(View): if not validate_caldav_proxy_path(path): return HttpResponse(status=400, content="Invalid path") - # Use user email as the principal (CalDAV server uses email as username) - user_principal = request.user.email + # Enforce channel path scope + if channel and not self._check_channel_path_access(channel, path): + return HttpResponse(status=403, content="Path not allowed for this channel") http = CalDAVHTTPClient() @@ -95,7 +182,7 @@ class CalDAVProxyView(View): # Prepare headers — start with shared auth headers, add proxy-specific ones try: - headers = CalDAVHTTPClient.build_base_headers(user_principal) + headers = CalDAVHTTPClient.build_base_headers(effective_user) except ValueError: logger.error("CALDAV_OUTBOUND_API_KEY is not configured") return HttpResponse( @@ -112,7 +199,7 @@ class CalDAVProxyView(View): # Use CALDAV_CALLBACK_BASE_URL if configured (for Docker environments where # the CalDAV container needs to reach Django via internal network) callback_path = reverse("caldav-scheduling-callback") - callback_base_url = getattr(settings, "CALDAV_CALLBACK_BASE_URL", None) + callback_base_url = settings.CALDAV_CALLBACK_BASE_URL if callback_base_url: # Use configured internal URL (e.g., http://backend:8000) headers["X-CalDAV-Callback-URL"] = ( @@ -145,7 +232,7 @@ class CalDAVProxyView(View): "Forwarding %s request to CalDAV server: %s (user: %s)", request.method, target_url, - user_principal, + effective_user.email, ) response = requests.request( method=request.method, @@ -161,7 +248,7 @@ class CalDAVProxyView(View): if response.status_code == 401: logger.warning( "CalDAV server returned 401 for user %s at %s", - user_principal, + effective_user.email, target_url, ) @@ -182,7 +269,7 @@ class CalDAVProxyView(View): except requests.exceptions.RequestException as e: logger.error("CalDAV server proxy error: %s", str(e)) return HttpResponse( - content=f"CalDAV server error: {str(e)}", + content="CalDAV server is unavailable", status=502, content_type="text/plain", ) @@ -216,9 +303,8 @@ class CalDAVDiscoveryView(View): # Clients need to discover the CalDAV URL before authenticating # Return redirect to CalDAV server base URL - caldav_base_url = f"/api/{settings.API_VERSION}/caldav/" response = HttpResponse(status=301) - response["Location"] = caldav_base_url + response["Location"] = "/caldav/" return response @@ -239,24 +325,25 @@ class CalDAVSchedulingCallbackView(View): See: https://sabre.io/dav/scheduling/ """ - def dispatch(self, request, *args, **kwargs): + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): # noqa: PLR0911 # pylint: disable=too-many-return-statements """Handle scheduling messages from CalDAV server.""" # Authenticate via API key api_key = request.headers.get("X-Api-Key", "").strip() expected_key = settings.CALDAV_INBOUND_API_KEY if not expected_key or not secrets.compare_digest(api_key, expected_key): - logger.warning( - "CalDAV scheduling callback request with invalid API key. " - "Expected: %s..., Got: %s...", - expected_key[:10] if expected_key else "None", - api_key[:10] if api_key else "None", - ) + logger.warning("CalDAV scheduling callback request with invalid API key.") return HttpResponse(status=401) - # Extract headers - sender = request.headers.get("X-CalDAV-Sender", "") - recipient = request.headers.get("X-CalDAV-Recipient", "") + # Extract and validate sender/recipient emails + sender = re.sub( + r"^mailto:", "", request.headers.get("X-CalDAV-Sender", "") + ).strip() + recipient = re.sub( + r"^mailto:", "", request.headers.get("X-CalDAV-Recipient", "") + ).strip() method = request.headers.get("X-CalDAV-Method", "").upper() # Validate required fields @@ -275,6 +362,22 @@ class CalDAVSchedulingCallbackView(View): content_type="text/plain", ) + # Validate email format + try: + validate_email(sender) + validate_email(recipient) + except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught + logger.warning( + "CalDAV scheduling callback with invalid email: sender=%s, recipient=%s", + sender, + recipient, + ) + return HttpResponse( + status=400, + content="Invalid sender or recipient email", + content_type="text/plain", + ) + # Get iCalendar data from request body icalendar_data = ( request.body.decode("utf-8", errors="replace") if request.body else "" @@ -332,6 +435,6 @@ class CalDAVSchedulingCallbackView(View): logger.exception("Error processing CalDAV scheduling callback: %s", e) return HttpResponse( status=500, - content=f"Internal error: {str(e)}", + content="Internal server error", content_type="text/plain", ) diff --git a/src/backend/core/api/viewsets_channels.py b/src/backend/core/api/viewsets_channels.py new file mode 100644 index 0000000..b23994e --- /dev/null +++ b/src/backend/core/api/viewsets_channels.py @@ -0,0 +1,148 @@ +"""Channel API for managing integration tokens.""" + +import logging +import secrets + +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from core import models +from core.api import serializers +from core.services.caldav_service import verify_caldav_access + +logger = logging.getLogger(__name__) + + +class ChannelViewSet(viewsets.GenericViewSet): + """CRUD for integration channels. + + Endpoints: + GET /api/v1.0/channels/ — list (filterable by ?type=) + POST /api/v1.0/channels/ — create (returns token once) + GET /api/v1.0/channels/{id}/ — retrieve + DELETE /api/v1.0/channels/{id}/ — delete + POST /api/v1.0/channels/{id}/regenerate-token/ — regenerate token + """ + + permission_classes = [IsAuthenticated] + serializer_class = serializers.ChannelSerializer + + def get_queryset(self): + return models.Channel.objects.filter(user=self.request.user).select_related( + "organization", "user" + ) + + def list(self, request): + """List channels created by the current user, optionally filtered by type.""" + queryset = self.get_queryset() + channel_type = request.query_params.get("type") + if channel_type: + queryset = queryset.filter(type=channel_type) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + def create(self, request): + """Create a new channel and return the token (once). + + For type="ical-feed", returns an existing channel if one already + exists for the same user + caldav_path (get-or-create semantics). + """ + create_serializer = serializers.ChannelCreateSerializer(data=request.data) + create_serializer.is_valid(raise_exception=True) + data = create_serializer.validated_data + + caldav_path = data.get("caldav_path", "") + channel_type = data.get("type", "caldav") + calendar_name = data.get("calendar_name", "") + + # If a caldav_path is specified, verify the user has access + if caldav_path and not verify_caldav_access(request.user, caldav_path): + return Response( + {"detail": "You don't have access to this calendar."}, + status=status.HTTP_403_FORBIDDEN, + ) + + # For ical-feed, return existing channel if one exists + if channel_type == "ical-feed" and caldav_path: + existing = ( + self.get_queryset() + .filter(caldav_path=caldav_path, type="ical-feed") + .first() + ) + if existing: + # Update calendar_name if provided and different + current_name = existing.settings.get("calendar_name", "") + if calendar_name and current_name != calendar_name: + existing.settings["calendar_name"] = calendar_name + existing.name = calendar_name + existing.save(update_fields=["settings", "name", "updated_at"]) + serializer = self.get_serializer(existing, context={"request": request}) + return Response(serializer.data, status=status.HTTP_200_OK) + + token = secrets.token_urlsafe(16) + channel_settings = {"role": data.get("role", models.Channel.ROLE_READER)} + if calendar_name: + channel_settings["calendar_name"] = calendar_name + + channel = models.Channel( + name=data.get("name") or calendar_name or caldav_path or "Channel", + type=channel_type, + user=request.user, + caldav_path=caldav_path, + organization=request.user.organization, + settings=channel_settings, + encrypted_settings={"token": token}, + ) + channel.save() + + # Attach plaintext token for the response (not persisted) + channel.token = token + serializer = serializers.ChannelWithTokenSerializer( + channel, context={"request": request} + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def retrieve(self, request, pk=None): + """Retrieve a channel (without token).""" + channel = self._get_owned_channel(pk) + if channel is None: + return Response(status=status.HTTP_404_NOT_FOUND) + serializer = self.get_serializer(channel, context={"request": request}) + return Response(serializer.data) + + def destroy(self, request, pk=None): + """Delete a channel.""" + channel = self._get_owned_channel(pk) + if channel is None: + return Response(status=status.HTTP_404_NOT_FOUND) + channel.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=True, methods=["post"], url_path="regenerate-token") + def regenerate_token(self, request, pk=None): + """Regenerate the token for an existing channel.""" + channel = self._get_owned_channel(pk) + if channel is None: + return Response(status=status.HTTP_404_NOT_FOUND) + + token = secrets.token_urlsafe(16) + channel.encrypted_settings = { + **channel.encrypted_settings, + "token": token, + } + channel.save(update_fields=["encrypted_settings", "updated_at"]) + + channel.token = token + serializer = serializers.ChannelWithTokenSerializer( + channel, context={"request": request} + ) + return Response(serializer.data) + + def _get_owned_channel(self, pk): + """Get a channel owned by the current user, or None.""" + try: + return self.get_queryset().get(pk=pk) + except models.Channel.DoesNotExist: + return None diff --git a/src/backend/core/api/viewsets_ical.py b/src/backend/core/api/viewsets_ical.py index 10b28cf..6dd4085 100644 --- a/src/backend/core/api/viewsets_ical.py +++ b/src/backend/core/api/viewsets_ical.py @@ -2,19 +2,24 @@ import logging +from django.core.cache import cache from django.http import Http404, HttpResponse from django.utils import timezone from django.utils.decorators import method_decorator +from django.utils.text import slugify from django.views import View from django.views.decorators.csrf import csrf_exempt import requests -from core.models import CalendarSubscriptionToken +from core.models import Channel, urlsafe_to_uuid from core.services.caldav_service import CalDAVHTTPClient logger = logging.getLogger(__name__) +ICAL_RATE_LIMIT = 5 # requests per minute per channel +ICAL_RATE_WINDOW = 60 # seconds + @method_decorator(csrf_exempt, name="dispatch") class ICalExportView(View): @@ -22,40 +27,49 @@ class ICalExportView(View): Public endpoint for iCal calendar exports. This view serves calendar data in iCal format without requiring authentication. - The token in the URL path acts as the authentication mechanism. + The channel_id in the URL is used for lookup, and the token for authentication. - URL format: /ical/.ics + URL format: /ical///.ics - The view proxies the request to SabreDAV's ICSExportPlugin, which generates - RFC 5545 compliant iCal data. + Looks up a Channel by base64url-encoded ID, verifies the token, then + proxies the request to SabreDAV's ICSExportPlugin. """ - def get(self, request, token): + def get(self, request, short_id, token): """Handle GET requests for iCal export.""" - # Lookup token - subscription = ( - CalendarSubscriptionToken.objects.filter(token=token, is_active=True) - .select_related("owner") - .first() - ) + try: + channel_id = urlsafe_to_uuid(short_id) + channel = Channel.objects.get(pk=channel_id, is_active=True) + except (ValueError, Channel.DoesNotExist) as exc: + raise Http404("Calendar not found") from exc - if not subscription: - logger.warning("Invalid or inactive subscription token: %s", token) + if channel.type != "ical-feed": raise Http404("Calendar not found") - # Update last_accessed_at atomically to avoid race conditions - # when multiple calendar clients poll simultaneously - CalendarSubscriptionToken.objects.filter(token=token, is_active=True).update( - last_accessed_at=timezone.now() - ) + if not channel.verify_token(token): + raise Http404("Calendar not found") + + if not channel.user: + logger.warning("ical-feed channel %s has no user", channel.id) + raise Http404("Calendar not found") + + # Rate limit: 5 requests per minute per channel + rate_key = f"ical_rate:{channel_id}" + hits = cache.get(rate_key, 0) + if hits >= ICAL_RATE_LIMIT: + return HttpResponse(status=429, content="Too many requests") + cache.set(rate_key, hits + 1, ICAL_RATE_WINDOW) + + # Update last_used_at + Channel.objects.filter(pk=channel.pk).update(last_used_at=timezone.now()) # Proxy to SabreDAV http = CalDAVHTTPClient() try: - caldav_path = subscription.caldav_path.lstrip("/") + caldav_path = channel.caldav_path.lstrip("/") response = http.request( "GET", - subscription.owner.email, + channel.user, caldav_path, query="export", ) @@ -88,15 +102,12 @@ class ICalExportView(View): status=200, content_type="text/calendar; charset=utf-8", ) - # Set filename for download (use calendar_name or fallback to "calendar") - display_name = subscription.calendar_name or "calendar" - safe_name = display_name.replace('"', '\\"') + calendar_name = channel.settings.get("calendar_name", "") + filename = slugify(calendar_name)[:50] or "feed" django_response["Content-Disposition"] = ( - f'attachment; filename="{safe_name}.ics"' + f'attachment; filename="{filename}.ics"' ) - # Prevent caching of potentially sensitive data django_response["Cache-Control"] = "no-store, private" - # Prevent token leakage via referrer django_response["Referrer-Policy"] = "no-referrer" return django_response diff --git a/src/backend/core/api/viewsets_rsvp.py b/src/backend/core/api/viewsets_rsvp.py index 7a6731e..11f8ca8 100644 --- a/src/backend/core/api/viewsets_rsvp.py +++ b/src/backend/core/api/viewsets_rsvp.py @@ -1,16 +1,28 @@ -"""RSVP view for handling invitation responses from email links.""" +"""RSVP view for handling invitation responses from email links. + +GET /rsvp/?token=...&action=accepted -> renders a confirmation page that + auto-submits via JavaScript (no extra click for the user). +POST /api/v1.0/rsvp/ -> processes the RSVP and returns a + result page. Link previewers / prefetchers only issue GET, so the + state-changing work is safely behind POST. +""" import logging import re from datetime import timezone as dt_timezone -from django.core.signing import BadSignature, Signer +from django.conf import settings +from django.core.signing import BadSignature, SignatureExpired, TimestampSigner from django.shortcuts import render from django.utils import timezone from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt +from rest_framework.throttling import AnonRateThrottle +from rest_framework.views import APIView + +from core.models import User from core.services.caldav_service import CalDAVHTTPClient from core.services.translation_service import TranslationService @@ -35,7 +47,7 @@ PARTSTAT_VALUES = { } -def _render_error(request, message, lang="fr"): +def _render_error(request, message, lang="en"): """Render the RSVP error page.""" t = TranslationService.t return render( @@ -85,69 +97,169 @@ def _is_event_past(icalendar_data): return False -@method_decorator(csrf_exempt, name="dispatch") -class RSVPView(View): - """Handle RSVP responses from invitation email links.""" +def _validate_token(token, max_age=None): + """Unsign and validate an RSVP token. - def get(self, request): # noqa: PLR0911 # pylint: disable=too-many-return-statements - """Process an RSVP response.""" + Returns (payload, error_key). On success error_key is None. + """ + ts_signer = TimestampSigner(salt="rsvp") + try: + payload = ts_signer.unsign_object(token, max_age=max_age) + except SignatureExpired: + return None, "token_expired" + except BadSignature: + return None, "invalid_token" + + uid = payload.get("uid") + recipient_email = payload.get("email") + organizer_email = payload.get("organizer", "") + # Strip mailto: prefix (case-insensitive) in case it leaked into the token + organizer_email = re.sub(r"^mailto:", "", organizer_email, flags=re.IGNORECASE) + + if not uid or not recipient_email or not organizer_email: + return None, "invalid_payload" + + payload["organizer"] = organizer_email + return payload, None + + +_TOKEN_ERROR_KEYS = { + "token_expired": "rsvp.error.tokenExpired", + "invalid_token": "rsvp.error.invalidToken", + "invalid_payload": "rsvp.error.invalidPayload", +} + + +def _validate_and_render_error(request, token, action, lang): + """Validate action + token; return (payload, error_response). + + On success error_response is None. + """ + t = TranslationService.t + + if action not in PARTSTAT_VALUES: + return None, _render_error(request, t("rsvp.error.invalidAction", lang), lang) + + payload, error = _validate_token( + token, max_age=settings.RSVP_TOKEN_MAX_AGE_RECURRING + ) + if error: + return None, _render_error(request, t(_TOKEN_ERROR_KEYS[error], lang), lang) + + return payload, None + + +@method_decorator(csrf_exempt, name="dispatch") +class RSVPConfirmView(View): + """GET handler: render auto-submitting confirmation page. + + This page is safe for link previewers / prefetchers because it + doesn't change any state — only the POST endpoint does. + """ + + def get(self, request): + """Render a page that auto-submits the RSVP via POST.""" token = request.GET.get("token", "") action = request.GET.get("action", "") lang = TranslationService.resolve_language(request=request) + + _, error_response = _validate_and_render_error(request, token, action, lang) + if error_response: + return error_response + + # Render auto-submit page + label = TranslationService.t(f"rsvp.{action}", lang) + return render( + request, + "rsvp/confirm.html", + { + "page_title": label, + "token": token, + "action": action, + "lang": lang, + "heading": label, + "status_icon": PARTSTAT_ICONS[action], + "header_color": PARTSTAT_COLORS[action], + "submit_label": label, + "post_url": f"/api/{settings.API_VERSION}/rsvp/", + }, + ) + + +class RSVPThrottle(AnonRateThrottle): + """Throttle RSVP POST requests: 30/min per IP.""" + + rate = "30/minute" + + +def _process_rsvp(request, payload, action, lang): + """Execute the RSVP: find event, update PARTSTAT, PUT back. + + Returns an error response on failure, or the updated calendar data + string on success. + """ + t = TranslationService.t + http = CalDAVHTTPClient() + + try: + organizer = User.objects.get(email=payload["organizer"]) + except User.DoesNotExist: + return _render_error(request, t("rsvp.error.eventNotFound", lang), lang) + + calendar_data, href, etag = http.find_event_by_uid(organizer, payload["uid"]) + if not calendar_data or not href: + return _render_error(request, t("rsvp.error.eventNotFound", lang), lang) + + if _is_event_past(calendar_data): + return _render_error(request, t("rsvp.error.eventPast", lang), lang) + + updated_data = CalDAVHTTPClient.update_attendee_partstat( + calendar_data, payload["email"], PARTSTAT_VALUES[action] + ) + if not updated_data: + return _render_error(request, t("rsvp.error.notAttendee", lang), lang) + + if not http.put_event(organizer, href, updated_data, etag=etag): + return _render_error(request, t("rsvp.error.updateFailed", lang), lang) + + return calendar_data + + +class RSVPProcessView(APIView): + """POST handler: actually process the RSVP. + + Uses DRF's AnonRateThrottle for rate limiting. No authentication + required — the signed token acts as authorization. + """ + + authentication_classes = [] + permission_classes = [] + throttle_classes = [RSVPThrottle] + + def post(self, request): + """Process the RSVP response.""" + token = request.data.get("token", "") + action = request.data.get("action", "") + lang = TranslationService.resolve_language(request=request) t = TranslationService.t - # Validate action - if action not in PARTSTAT_VALUES: - return _render_error(request, t("rsvp.error.invalidAction", lang), lang) - - # Unsign token — tokens don't have a built-in expiry, - # but RSVPs are rejected once the event has ended (_is_event_past). - signer = Signer(salt="rsvp") - try: - payload = signer.unsign_object(token) - except BadSignature: - return _render_error(request, t("rsvp.error.invalidToken", lang), lang) - - uid = payload.get("uid") - recipient_email = payload.get("email") - # Strip mailto: prefix (case-insensitive) in case it leaked into the token - organizer_email = re.sub( - r"^mailto:", "", payload.get("organizer", ""), flags=re.IGNORECASE + payload, error_response = _validate_and_render_error( + request, token, action, lang ) + if error_response: + return error_response - if not uid or not recipient_email or not organizer_email: - return _render_error(request, t("rsvp.error.invalidPayload", lang), lang) + result = _process_rsvp(request, payload, action, lang) - http = CalDAVHTTPClient() + # result is either an error HttpResponse or calendar data string + if not isinstance(result, str): + return result - # Find the event in the organizer's CalDAV calendars - calendar_data, href = http.find_event_by_uid(organizer_email, uid) - if not calendar_data or not href: - return _render_error(request, t("rsvp.error.eventNotFound", lang), lang) - - # Check if the event is already over - if _is_event_past(calendar_data): - return _render_error(request, t("rsvp.error.eventPast", lang), lang) - - # Update the attendee's PARTSTAT - partstat = PARTSTAT_VALUES[action] - updated_data = CalDAVHTTPClient.update_attendee_partstat( - calendar_data, recipient_email, partstat - ) - if not updated_data: - return _render_error(request, t("rsvp.error.notAttendee", lang), lang) - - # PUT the updated event back to CalDAV - success = http.put_event(organizer_email, href, updated_data) - if not success: - return _render_error(request, t("rsvp.error.updateFailed", lang), lang) - - # Extract event summary for display from core.services.calendar_invitation_service import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel ICalendarParser, ) - summary = ICalendarParser.extract_property(calendar_data, "SUMMARY") or "" + summary = ICalendarParser.extract_property(result, "SUMMARY") or "" label = t(f"rsvp.{action}", lang) return render( diff --git a/src/backend/core/api/viewsets_task.py b/src/backend/core/api/viewsets_task.py new file mode 100644 index 0000000..e1043e4 --- /dev/null +++ b/src/backend/core/api/viewsets_task.py @@ -0,0 +1,104 @@ +"""API endpoint for polling async task status.""" + +import logging +import uuid + +import dramatiq +from dramatiq.results import ResultFailure, ResultMissing +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from core.task_utils import get_task_progress, get_task_tracking + +logger = logging.getLogger(__name__) + + +class TaskDetailView(APIView): + """View to retrieve the status of an async task.""" + + permission_classes = [IsAuthenticated] + + def get(self, request, task_id): # noqa: PLR0911 # pylint: disable=too-many-return-statements + """Get the status of a task.""" + try: + uuid.UUID(task_id) + except ValueError: + return Response( + {"status": "FAILURE", "result": None, "error": "Not found"}, + status=404, + ) + + tracking = get_task_tracking(task_id) + if tracking is None: + return Response( + {"status": "FAILURE", "result": None, "error": "Not found"}, + status=404, + ) + if str(request.user.id) != tracking["owner"]: + return Response( + {"status": "FAILURE", "result": None, "error": "Forbidden"}, + status=403, + ) + + # Try to fetch the result from dramatiq's result backend + message = dramatiq.Message( + queue_name=tracking["queue_name"], + actor_name=tracking["actor_name"], + args=(), + kwargs={}, + options={}, + message_id=task_id, + ) + try: + result_data = message.get_result(block=False) + except ResultMissing: + result_data = None + except ResultFailure as exc: + logger.error("Task %s failed: %s", task_id, exc) + return Response( + { + "status": "FAILURE", + "result": None, + "error": "Task failed", + } + ) + + if result_data is not None: + resp = { + "status": "SUCCESS", + "result": result_data, + "error": None, + } + # Unpack {status, result, error} convention + if ( + isinstance(result_data, dict) + and {"status", "result", "error"} <= result_data.keys() + ): + resp["status"] = result_data["status"] + resp["result"] = result_data["result"] + resp["error"] = result_data["error"] + return Response(resp) + + # Check for progress data + progress_data = get_task_progress(task_id) + if progress_data: + return Response( + { + "status": "PROGRESS", + "result": None, + "error": None, + "progress": progress_data.get("progress"), + "message": progress_data.get("metadata", {}).get("message"), + "timestamp": progress_data.get("timestamp"), + } + ) + + # Default to pending + return Response( + { + "status": "PENDING", + "result": None, + "error": None, + } + ) diff --git a/src/backend/core/apps.py b/src/backend/core/apps.py index 121bcb2..565a339 100644 --- a/src/backend/core/apps.py +++ b/src/backend/core/apps.py @@ -1,7 +1,6 @@ """Calendars Core application""" from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ class CoreConfig(AppConfig): @@ -9,7 +8,7 @@ class CoreConfig(AppConfig): name = "core" app_label = "core" - verbose_name = _("calendars core application") + verbose_name = "calendars core application" def ready(self): """ diff --git a/src/backend/core/authentication/backends.py b/src/backend/core/authentication/backends.py index 1faa27c..7ae60f8 100644 --- a/src/backend/core/authentication/backends.py +++ b/src/backend/core/authentication/backends.py @@ -10,11 +10,51 @@ from lasuite.oidc_login.backends import ( ) from core.entitlements import EntitlementsUnavailableError, get_user_entitlements -from core.models import DuplicateEmailError +from core.models import DuplicateEmailError, Organization logger = logging.getLogger(__name__) +def _resolve_org_external_id(claims, email=None): + """Extract the organization external_id from OIDC claims or email domain.""" + claim_key = settings.OIDC_USERINFO_ORGANIZATION_CLAIM + if claim_key: + return claims.get(claim_key) + email = email or claims.get("email") + return email.split("@")[-1] if email and "@" in email else None + + +def resolve_organization(user, claims, entitlements=None): + """Resolve and assign the user's organization. + + The org identifier (external_id) comes from the OIDC claim configured via + OIDC_USERINFO_ORGANIZATION_CLAIM, or falls back to the email domain. + The org name comes from the entitlements response. + """ + entitlements = entitlements or {} + external_id = _resolve_org_external_id(claims, email=user.email) + if not external_id: + logger.error( + "Cannot resolve organization for user %s: no org claim or email domain", + user.email, + ) + return + + org_name = entitlements.get("organization_name", "") or external_id + + org, created = Organization.objects.get_or_create( + external_id=external_id, + defaults={"name": org_name}, + ) + if not created and org_name and org.name != org_name: + org.name = org_name + org.save(update_fields=["name"]) + + if user.organization_id != org.id: + user.organization = org + user.save(update_fields=["organization"]) + + class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend): """Custom OpenID Connect (OIDC) Authentication Backend. @@ -23,39 +63,46 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend): """ def get_extra_claims(self, user_info): - """ - Return extra claims from user_info. - - Args: - user_info (dict): The user information dictionary. - - Returns: - dict: A dictionary of extra claims. - """ - - # We need to add the claims that we want to store so that they are - # available in the post_get_or_create_user method. + """Return extra claims from user_info.""" claims_to_store = { claim: user_info.get(claim) for claim in settings.OIDC_STORE_CLAIMS } return { "full_name": self.compute_full_name(user_info), - "short_name": user_info.get(settings.OIDC_USERINFO_SHORTNAME_FIELD), "claims": claims_to_store, } def get_existing_user(self, sub, email): """Fetch existing user by sub or email.""" - try: return self.UserModel.objects.get_user_by_sub_or_email(sub, email) except DuplicateEmailError as err: raise SuspiciousOperation(err.message) from err + def create_user(self, claims): + """Create a new user, resolving their organization first. + + Organization is NOT NULL, so we must resolve it before the initial save. + """ + external_id = _resolve_org_external_id(claims) + if not external_id: + raise SuspiciousOperation( + "Cannot create user without an organization " + "(no org claim and no email domain)" + ) + + org, _ = Organization.objects.get_or_create( + external_id=external_id, + defaults={"name": external_id}, + ) + claims["organization"] = org + return super().create_user(claims) + def post_get_or_create_user(self, user, claims, is_new_user): - """Warm the entitlements cache on login (force_refresh).""" + """Warm the entitlements cache and resolve organization on login.""" + entitlements = {} try: - get_user_entitlements( + entitlements = get_user_entitlements( user_sub=user.sub, user_email=user.email, user_info=claims, @@ -66,3 +113,5 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend): "Entitlements unavailable for %s during login", user.email, ) + + resolve_organization(user, claims, entitlements) diff --git a/src/backend/core/entitlements/__init__.py b/src/backend/core/entitlements/__init__.py index 36cd45f..a67340b 100644 --- a/src/backend/core/entitlements/__init__.py +++ b/src/backend/core/entitlements/__init__.py @@ -17,7 +17,7 @@ def get_user_entitlements(user_sub, user_email, user_info=None, force_refresh=Fa force_refresh: If True, bypass backend cache and fetch fresh data. Returns: - dict: {"can_access": bool} + dict: {"can_access": bool, "can_admin": bool, ...} Raises: EntitlementsUnavailableError: If the backend cannot be reached diff --git a/src/backend/core/entitlements/backends/base.py b/src/backend/core/entitlements/backends/base.py index fbeea6a..12e5460 100644 --- a/src/backend/core/entitlements/backends/base.py +++ b/src/backend/core/entitlements/backends/base.py @@ -20,7 +20,11 @@ class EntitlementsBackend(ABC): force_refresh: If True, bypass any cache and fetch fresh data. Returns: - dict: {"can_access": bool} + dict: { + "can_access": bool, + "can_admin": bool, + "organization_name": str, # optional, extracted from response + } Raises: EntitlementsUnavailableError: If the backend cannot be reached. diff --git a/src/backend/core/entitlements/backends/deploycenter.py b/src/backend/core/entitlements/backends/deploycenter.py index 43f5720..a07e6c8 100644 --- a/src/backend/core/entitlements/backends/deploycenter.py +++ b/src/backend/core/entitlements/backends/deploycenter.py @@ -114,7 +114,14 @@ class DeployCenterEntitlementsBackend(EntitlementsBackend): entitlements = data.get("entitlements", {}) result = { "can_access": entitlements.get("can_access", False), + "can_admin": entitlements.get("can_admin", False), } + # Organization name from DeployCenter response (if present) + org = data.get("organization") or {} + org_name = org.get("name", "") + if org_name: + result["organization_name"] = org_name + cache.set(cache_key, result, settings.ENTITLEMENTS_CACHE_TIMEOUT) return result diff --git a/src/backend/core/entitlements/backends/local.py b/src/backend/core/entitlements/backends/local.py index 370a600..962b381 100644 --- a/src/backend/core/entitlements/backends/local.py +++ b/src/backend/core/entitlements/backends/local.py @@ -4,9 +4,9 @@ from core.entitlements.backends.base import EntitlementsBackend class LocalEntitlementsBackend(EntitlementsBackend): - """Local backend that always grants access.""" + """Local backend that always grants access and admin.""" def get_user_entitlements( self, user_sub, user_email, user_info=None, force_refresh=False ): - return {"can_access": True} + return {"can_access": True, "can_admin": True} diff --git a/src/backend/core/enums.py b/src/backend/core/enums.py index 8f7e70c..e49721c 100644 --- a/src/backend/core/enums.py +++ b/src/backend/core/enums.py @@ -1,12 +1,3 @@ """ Core application enums declaration """ - -from django.conf import global_settings -from django.utils.translation import gettext_lazy as _ - -# In Django's code base, `LANGUAGES` is set by default with all supported languages. -# We can use it for the choice of languages which should not be limited to the few languages -# active in the app. -# pylint: disable=no-member -ALL_LANGUAGES = {language: _(name) for language, name in global_settings.LANGUAGES} diff --git a/src/backend/core/external_api/viewsets.py b/src/backend/core/external_api/viewsets.py index 1d6e4c7..b90f6b5 100644 --- a/src/backend/core/external_api/viewsets.py +++ b/src/backend/core/external_api/viewsets.py @@ -4,7 +4,7 @@ from django.conf import settings from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication -from core.api.permissions import AccessPermission, IsSelf +from core.api.permissions import IsSelf from core.api.viewsets import UserViewSet from core.external_api.permissions import ResourceServerClientPermission diff --git a/src/backend/core/factories.py b/src/backend/core/factories.py index 4148700..37bfa82 100644 --- a/src/backend/core/factories.py +++ b/src/backend/core/factories.py @@ -2,6 +2,8 @@ Core application factories """ +import secrets + from django.conf import settings from django.contrib.auth.hashers import make_password @@ -13,6 +15,16 @@ from core import models fake = Faker() +class OrganizationFactory(factory.django.DjangoModelFactory): + """A factory to create organizations for testing purposes.""" + + class Meta: + model = models.Organization + + name = factory.Faker("company") + external_id = factory.Sequence(lambda n: f"org-{n}") + + class UserFactory(factory.django.DjangoModelFactory): """A factory to random users for testing purposes.""" @@ -23,20 +35,32 @@ class UserFactory(factory.django.DjangoModelFactory): sub = factory.Sequence(lambda n: f"user{n!s}") email = factory.Faker("email") full_name = factory.Faker("name") - short_name = factory.Faker("first_name") language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES]) password = make_password("password") + organization = factory.SubFactory(OrganizationFactory) -class CalendarSubscriptionTokenFactory(factory.django.DjangoModelFactory): - """A factory to create calendar subscription tokens for testing purposes.""" +class ChannelFactory(factory.django.DjangoModelFactory): + """A factory to create channels for testing purposes.""" class Meta: - model = models.CalendarSubscriptionToken + model = models.Channel - owner = factory.SubFactory(UserFactory) - caldav_path = factory.LazyAttribute( - lambda obj: f"/calendars/{obj.owner.email}/{fake.uuid4()}/" + name = factory.Faker("sentence", nb_words=3) + user = factory.SubFactory(UserFactory) + settings = factory.LazyFunction(lambda: {"role": "reader"}) + encrypted_settings = factory.LazyFunction( + lambda: {"token": secrets.token_urlsafe(16)} + ) + + +class ICalFeedChannelFactory(ChannelFactory): + """A factory to create ical-feed channels.""" + + type = "ical-feed" + caldav_path = factory.LazyAttribute( + lambda obj: f"/calendars/users/{obj.user.email}/{fake.uuid4()}/" + ) + settings = factory.LazyAttribute( + lambda obj: {"role": "reader", "calendar_name": fake.sentence(nb_words=3)} ) - calendar_name = factory.Faker("sentence", nb_words=3) - is_active = True diff --git a/src/backend/core/migrations/0001_initial.py b/src/backend/core/migrations/0001_initial.py index 9315338..2282766 100644 --- a/src/backend/core/migrations/0001_initial.py +++ b/src/backend/core/migrations/0001_initial.py @@ -1,8 +1,9 @@ -# Generated by Django 5.2.9 on 2026-01-11 00:45 +# Generated by Django 5.2.9 on 2026-03-08 21:40 import core.models import django.core.validators import django.db.models.deletion +import encrypted_fields.fields import timezone_field.fields import uuid from django.conf import settings @@ -18,6 +19,21 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Organization', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), + ('name', models.CharField(blank=True, default='', max_length=200)), + ('external_id', models.CharField(db_index=True, help_text='Organization identifier from OIDC claim or email domain.', max_length=128, unique=True)), + ], + options={ + 'verbose_name': 'organization', + 'verbose_name_plural': 'organizations', + 'db_table': 'calendars_organization', + }, + ), migrations.CreateModel( name='User', fields=[ @@ -29,8 +45,7 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), ('sub', models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.', regex='^[\\w.@+-:]+\\Z')], verbose_name='sub')), ('full_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='full name')), - ('short_name', models.CharField(blank=True, max_length=20, null=True, verbose_name='short name')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')), + ('email', models.EmailField(blank=True, db_index=True, max_length=254, null=True, verbose_name='identity email address')), ('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')), ('language', models.CharField(blank=True, choices=[('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'), ('nl-nl', 'Dutch')], default=None, help_text='The language in which the user wants to see the interface.', max_length=10, null=True, verbose_name='language')), ('timezone', timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='UTC', help_text='The timezone in which the user wants to see times.', use_pytz=False)), @@ -40,6 +55,7 @@ class Migration(migrations.Migration): ('claims', models.JSONField(blank=True, default=dict, help_text='Claims from the OIDC token.')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ('organization', models.ForeignKey(help_text='The organization this user belongs to.', on_delete=django.db.models.deletion.PROTECT, related_name='members', to='core.organization')), ], options={ 'verbose_name': 'user', @@ -51,40 +67,26 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Calendar', + name='Channel', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=255)), - ('color', models.CharField(default='#3174ad', max_length=7)), - ('description', models.TextField(blank=True, default='')), - ('is_default', models.BooleanField(default=False)), - ('is_visible', models.BooleanField(default=True)), - ('caldav_path', models.CharField(max_length=512, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calendars', to=settings.AUTH_USER_MODEL)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')), + ('name', models.CharField(help_text='Human-readable name for this channel.', max_length=255)), + ('type', models.CharField(default='caldav', help_text='Type of channel.', max_length=255)), + ('caldav_path', models.CharField(blank=True, default='', help_text='CalDAV path scope (e.g. /calendars/users/user@ex.com/cal/).', max_length=512)), + ('is_active', models.BooleanField(default=True)), + ('settings', models.JSONField(blank=True, default=dict, help_text='Channel-specific configuration settings (e.g. role).', verbose_name='settings')), + ('encrypted_settings', encrypted_fields.fields.EncryptedJSONField(blank=True, default=dict, help_text='Encrypted channel settings (e.g. token).', verbose_name='encrypted settings')), + ('last_used_at', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(blank=True, help_text='User who created this channel (used for permissions and auditing).', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='channels', to=settings.AUTH_USER_MODEL)), + ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='channels', to='core.organization')), ], options={ - 'ordering': ['-is_default', 'name'], + 'verbose_name': 'channel', + 'verbose_name_plural': 'channels', + 'db_table': 'calendars_channel', + 'ordering': ['-created_at'], }, ), - migrations.CreateModel( - name='CalendarShare', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('permission', models.CharField(choices=[('read', 'Read only'), ('write', 'Read and write')], default='read', max_length=10)), - ('is_visible', models.BooleanField(default=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('calendar', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shares', to='core.calendar')), - ('shared_with', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shared_calendars', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AddConstraint( - model_name='calendar', - constraint=models.UniqueConstraint(condition=models.Q(('is_default', True)), fields=('owner',), name='unique_default_calendar_per_user'), - ), - migrations.AlterUniqueTogether( - name='calendarshare', - unique_together={('calendar', 'shared_with')}, - ), ] diff --git a/src/backend/core/migrations/0002_calendarsubscriptiontoken.py b/src/backend/core/migrations/0002_calendarsubscriptiontoken.py deleted file mode 100644 index 5ad4c8d..0000000 --- a/src/backend/core/migrations/0002_calendarsubscriptiontoken.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-25 14:21 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='CalendarSubscriptionToken', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('caldav_path', models.CharField(help_text='CalDAV path of the calendar', max_length=512)), - ('calendar_name', models.CharField(blank=True, default='', help_text='Display name of the calendar', max_length=255)), - ('token', models.UUIDField(db_index=True, default=uuid.uuid4, help_text='Secret token used in the subscription URL', unique=True)), - ('is_active', models.BooleanField(default=True, help_text='Whether this subscription token is active')), - ('last_accessed_at', models.DateTimeField(blank=True, help_text='Last time this subscription URL was accessed', null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscription_tokens', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'calendar subscription token', - 'verbose_name_plural': 'calendar subscription tokens', - 'constraints': [models.UniqueConstraint(fields=('owner', 'caldav_path'), name='unique_token_per_owner_calendar')], - }, - ), - ] diff --git a/src/backend/core/migrations/0003_calendarsubscriptiontoken_token_active_idx.py b/src/backend/core/migrations/0003_calendarsubscriptiontoken_token_active_idx.py deleted file mode 100644 index 994ac29..0000000 --- a/src/backend/core/migrations/0003_calendarsubscriptiontoken_token_active_idx.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.9 on 2026-01-25 15:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0002_calendarsubscriptiontoken'), - ] - - operations = [ - migrations.AddIndex( - model_name='calendarsubscriptiontoken', - index=models.Index(fields=['token', 'is_active'], name='token_active_idx'), - ), - ] diff --git a/src/backend/core/migrations/0004_remove_calendarshare_calendar_and_more.py b/src/backend/core/migrations/0004_remove_calendarshare_calendar_and_more.py deleted file mode 100644 index c4ecf00..0000000 --- a/src/backend/core/migrations/0004_remove_calendarshare_calendar_and_more.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.9 on 2026-02-09 18:23 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0003_calendarsubscriptiontoken_token_active_idx'), - ] - - operations = [ - migrations.DeleteModel( - name='CalendarShare', - ), - migrations.DeleteModel( - name='Calendar', - ), - ] diff --git a/src/backend/core/models.py b/src/backend/core/models.py index a23bc21..ce80b8b 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -2,6 +2,8 @@ Declare and configure the models for the calendars core application """ +import base64 +import secrets import uuid from logging import getLogger @@ -10,47 +12,13 @@ from django.contrib.auth import models as auth_models from django.contrib.auth.base_user import AbstractBaseUser from django.core import mail, validators from django.db import models -from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy as _ +from encrypted_fields.fields import EncryptedJSONField from timezone_field import TimeZoneField logger = getLogger(__name__) -class LinkRoleChoices(models.TextChoices): - """Defines the possible roles a link can offer on a item.""" - - READER = "reader", _("Reader") # Can read - EDITOR = "editor", _("Editor") # Can read and edit - - -class RoleChoices(models.TextChoices): - """Defines the possible roles a user can have in a resource.""" - - READER = "reader", _("Reader") # Can read - EDITOR = "editor", _("Editor") # Can read and edit - ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share - OWNER = "owner", _("Owner") - - -PRIVILEGED_ROLES = [RoleChoices.ADMIN, RoleChoices.OWNER] - - -class LinkReachChoices(models.TextChoices): - """Defines types of access for links""" - - RESTRICTED = ( - "restricted", - _("Restricted"), - ) # Only users with a specific access can read/edit the item - AUTHENTICATED = ( - "authenticated", - _("Authenticated"), - ) # Any authenticated user can access the item - PUBLIC = "public", _("Public") # Even anonymous users can access the item - - class DuplicateEmailError(Exception): """Raised when an email is already associated with a pre-existing user.""" @@ -70,21 +38,21 @@ class BaseModel(models.Model): """ id = models.UUIDField( - verbose_name=_("id"), - help_text=_("primary key for the record as UUID"), + verbose_name="id", + help_text="primary key for the record as UUID", primary_key=True, default=uuid.uuid4, editable=False, ) created_at = models.DateTimeField( - verbose_name=_("created on"), - help_text=_("date and time at which a record was created"), + verbose_name="created on", + help_text="date and time at which a record was created", auto_now_add=True, editable=False, ) updated_at = models.DateTimeField( - verbose_name=_("updated on"), - help_text=_("date and time at which a record was last updated"), + verbose_name="updated on", + help_text="date and time at which a record was last updated", auto_now=True, editable=False, ) @@ -98,6 +66,46 @@ class BaseModel(models.Model): super().save(*args, **kwargs) +class Organization(BaseModel): + """Organization model, populated from OIDC claims and entitlements. + + Every user belongs to exactly one organization, determined by their + email domain (default) or a configurable OIDC claim. Orgs are + created automatically on first login. + """ + + name = models.CharField(max_length=200, blank=True, default="") + external_id = models.CharField( + max_length=128, + unique=True, + db_index=True, + help_text="Organization identifier from OIDC claim or email domain.", + ) + + class Meta: + db_table = "calendars_organization" + verbose_name = "organization" + verbose_name_plural = "organizations" + + def __str__(self): + return self.name or self.external_id + + def delete(self, *args, **kwargs): + """Delete org after cleaning up members' CalDAV data. + + Must run before super().delete() because the User FK uses + on_delete=PROTECT, which blocks deletion while members exist. + The pre_delete signal would never fire with PROTECT, so the + cleanup logic lives here instead. + """ + from core.services.caldav_service import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + cleanup_organization_caldav_data, + ) + + cleanup_organization_caldav_data(self) + super().delete(*args, **kwargs) + + class UserManager(auth_models.UserManager): """Custom manager for User model with additional methods.""" @@ -119,10 +127,8 @@ class UserManager(auth_models.UserManager): and not settings.OIDC_ALLOW_DUPLICATE_EMAILS ): raise DuplicateEmailError( - _( - "We couldn't find a user with this sub but the email is already " - "associated with a registered user." - ) + "We couldn't find a user with this sub but the email is already " + "associated with a registered user." ) from err return None @@ -132,16 +138,17 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin): sub_validator = validators.RegexValidator( regex=r"^[\w.@+-:]+\Z", - message=_( + message=( "Enter a valid sub. This value may contain only letters, " "numbers, and @/./+/-/_/: characters." ), ) sub = models.CharField( - _("sub"), - help_text=_( - "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only." + "sub", + help_text=( + "Required. 255 characters or fewer." + " Letters, numbers, and @/./+/-/_/: characters only." ), max_length=255, unique=True, @@ -150,23 +157,24 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin): null=True, ) - full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True) - short_name = models.CharField(_("short name"), max_length=20, null=True, blank=True) + full_name = models.CharField("full name", max_length=100, null=True, blank=True) - email = models.EmailField(_("identity email address"), blank=True, null=True) + email = models.EmailField( + "identity email address", blank=True, null=True, db_index=True + ) # Unlike the "email" field which stores the email coming from the OIDC token, this field # stores the email used by staff users to login to the admin site admin_email = models.EmailField( - _("admin email address"), unique=True, blank=True, null=True + "admin email address", unique=True, blank=True, null=True ) language = models.CharField( max_length=10, choices=settings.LANGUAGES, default=None, - verbose_name=_("language"), - help_text=_("The language in which the user wants to see the interface."), + verbose_name="language", + help_text="The language in which the user wants to see the interface.", null=True, blank=True, ) @@ -174,31 +182,38 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin): choices_display="WITH_GMT_OFFSET", use_pytz=False, default=settings.TIME_ZONE, - help_text=_("The timezone in which the user wants to see times."), + help_text="The timezone in which the user wants to see times.", ) is_device = models.BooleanField( - _("device"), + "device", default=False, - help_text=_("Whether the user is a device or a real user."), + help_text="Whether the user is a device or a real user.", ) is_staff = models.BooleanField( - _("staff status"), + "staff status", default=False, - help_text=_("Whether the user can log into this admin site."), + help_text="Whether the user can log into this admin site.", ) is_active = models.BooleanField( - _("active"), + "active", default=True, - help_text=_( + help_text=( "Whether this user should be treated as active. " "Unselect this instead of deleting accounts." ), ) + organization = models.ForeignKey( + Organization, + on_delete=models.PROTECT, + related_name="members", + help_text="The organization this user belongs to.", + ) + claims = models.JSONField( blank=True, default=dict, - help_text=_("Claims from the OIDC token."), + help_text="Claims from the OIDC token.", ) objects = UserManager() @@ -208,8 +223,8 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin): class Meta: db_table = "calendars_user" - verbose_name = _("user") - verbose_name_plural = _("users") + verbose_name = "user" + verbose_name_plural = "users" def __str__(self): return self.email or self.admin_email or str(self.id) @@ -220,152 +235,123 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin): raise ValueError("User has no email address.") mail.send_mail(subject, message, from_email, [self.email], **kwargs) - @cached_property - def teams(self): - """ - Get list of teams in which the user is, as a list of strings. - Must be cached if retrieved remotely. - """ - return [] + +def uuid_to_urlsafe(u): + """Encode a UUID as unpadded base64url (22 chars).""" + return base64.urlsafe_b64encode(u.bytes).rstrip(b"=").decode() -class BaseAccess(BaseModel): - """Base model for accesses to handle resources.""" +def urlsafe_to_uuid(s): + """Decode an unpadded base64url string back to a UUID.""" + padded = s + "=" * (-len(s) % 4) + return uuid.UUID(bytes=base64.urlsafe_b64decode(padded)) + + +class Channel(BaseModel): + """Integration channel for external service access to calendars. + + Follows the same pattern as the Messages Channel model. Allows external + services (e.g. Messages) to access CalDAV on behalf of a user via a + bearer token. + + Configuration is split between ``settings`` (public, non-sensitive) and + ``encrypted_settings`` (sensitive data like tokens). The ``role`` for + CalDAV access control lives in ``settings``. + + For iCal feeds, the URL contains the base64url-encoded channel ID (for + lookup) and a base64url token (for authentication): + ``/ical///.ics``. + """ + + ROLE_READER = "reader" + ROLE_EDITOR = "editor" + ROLE_ADMIN = "admin" + VALID_ROLES = {ROLE_READER, ROLE_EDITOR, ROLE_ADMIN} + + name = models.CharField( + max_length=255, + help_text="Human-readable name for this channel.", + ) + + type = models.CharField( + max_length=255, + help_text="Type of channel.", + default="caldav", + ) user = models.ForeignKey( - User, + "User", on_delete=models.CASCADE, null=True, blank=True, - ) - team = models.CharField(max_length=100, blank=True) - role = models.CharField( - max_length=20, choices=RoleChoices.choices, default=RoleChoices.READER + related_name="channels", + help_text="User who created this channel (used for permissions and auditing).", ) - class Meta: - abstract = True - - def _get_abilities(self, resource, user): - """ - Compute and return abilities for a given user taking into account - the current state of the object. - """ - roles = [] - if user.is_authenticated: - teams = user.teams - try: - roles = self.user_roles or [] - except AttributeError: - try: - roles = resource.accesses.filter( - models.Q(user=user) | models.Q(team__in=teams), - ).values_list("role", flat=True) - except (self._meta.model.DoesNotExist, IndexError): - roles = [] - - is_owner_or_admin = bool( - set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN}) - ) - if self.role == RoleChoices.OWNER: - can_delete = ( - RoleChoices.OWNER in roles - and resource.accesses.filter(role=RoleChoices.OWNER).count() > 1 - ) - set_role_to = ( - [RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER] - if can_delete - else [] - ) - else: - can_delete = is_owner_or_admin - set_role_to = [] - if RoleChoices.OWNER in roles: - set_role_to.append(RoleChoices.OWNER) - if is_owner_or_admin: - set_role_to.extend( - [RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER] - ) - - # Remove the current role as we don't want to propose it as an option - try: - set_role_to.remove(self.role) - except ValueError: - pass - - return { - "destroy": can_delete, - "update": bool(set_role_to), - "partial_update": bool(set_role_to), - "retrieve": bool(roles), - "set_role_to": set_role_to, - } - - -class CalendarSubscriptionToken(models.Model): - """ - Stores subscription tokens for iCal export. - Each calendar can have one token that allows unauthenticated read-only access - via a public URL for use in external calendar applications. - - This model is standalone and stores the CalDAV path directly, - without requiring a foreign key to the Calendar model. - """ - - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - - # Owner of the calendar (for permission verification) - owner = models.ForeignKey( - settings.AUTH_USER_MODEL, + organization = models.ForeignKey( + Organization, on_delete=models.CASCADE, - related_name="subscription_tokens", + null=True, + blank=True, + related_name="channels", ) - # CalDAV path stored directly (e.g., /calendars/user@example.com/uuid/) caldav_path = models.CharField( max_length=512, - help_text=_("CalDAV path of the calendar"), - ) - - # Calendar display name (for UI display) - calendar_name = models.CharField( - max_length=255, blank=True, default="", - help_text=_("Display name of the calendar"), + help_text="CalDAV path scope (e.g. /calendars/users/user@ex.com/cal/).", ) - token = models.UUIDField( - unique=True, - db_index=True, - default=uuid.uuid4, - help_text=_("Secret token used in the subscription URL"), - ) - is_active = models.BooleanField( - default=True, - help_text=_("Whether this subscription token is active"), - ) - last_accessed_at = models.DateTimeField( - null=True, + is_active = models.BooleanField(default=True) + + settings = models.JSONField( + "settings", + default=dict, blank=True, - help_text=_("Last time this subscription URL was accessed"), + help_text="Channel-specific configuration settings (e.g. role).", ) - created_at = models.DateTimeField(auto_now_add=True) + + encrypted_settings = EncryptedJSONField( + "encrypted settings", + default=dict, + blank=True, + help_text="Encrypted channel settings (e.g. token).", + ) + + last_used_at = models.DateTimeField(null=True, blank=True) class Meta: - verbose_name = _("calendar subscription token") - verbose_name_plural = _("calendar subscription tokens") - constraints = [ - models.UniqueConstraint( - fields=["owner", "caldav_path"], - name="unique_token_per_owner_calendar", - ) - ] - indexes = [ - # Composite index for the public iCal endpoint query: - # CalendarSubscriptionToken.objects.filter(token=..., is_active=True) - models.Index(fields=["token", "is_active"], name="token_active_idx"), - ] + db_table = "calendars_channel" + verbose_name = "channel" + verbose_name_plural = "channels" + ordering = ["-created_at"] def __str__(self): - return f"Subscription token for {self.calendar_name or self.caldav_path}" + return self.name + + @property + def role(self): + """Get the role from settings, defaulting to reader.""" + return self.settings.get("role", self.ROLE_READER) + + @role.setter + def role(self, value): + """Set the role in settings.""" + self.settings["role"] = value + + def clean(self): + """Validate that at least one scope is set.""" + from django.core.exceptions import ValidationError # noqa: PLC0415, I001 # pylint: disable=C0415 + + if not self.organization and not self.user and not self.caldav_path: + raise ValidationError( + "At least one scope must be set: organization, user, or caldav_path." + ) + + def verify_token(self, token): + """Check that *token* matches the stored encrypted token.""" + stored = self.encrypted_settings.get("token", "") + if not token or not stored: + return False + return secrets.compare_digest(token, stored) diff --git a/src/backend/core/services/caldav_service.py b/src/backend/core/services/caldav_service.py index d170528..6c394ca 100644 --- a/src/backend/core/services/caldav_service.py +++ b/src/backend/core/services/caldav_service.py @@ -1,8 +1,10 @@ """Services for CalDAV integration.""" +import json import logging import re from datetime import date, datetime, timedelta +from datetime import timezone as dt_timezone from typing import Optional from urllib.parse import unquote from uuid import uuid4 @@ -30,7 +32,7 @@ class CalDAVHTTPClient: and HTTP requests. All higher-level CalDAV consumers delegate to this. """ - BASE_URI_PATH = "/api/v1.0/caldav" + BASE_URI_PATH = "/caldav" DEFAULT_TIMEOUT = 30 def __init__(self): @@ -45,11 +47,21 @@ class CalDAVHTTPClient: return key @classmethod - def build_base_headers(cls, email: str) -> dict: - """Build authentication headers for CalDAV requests.""" + def build_base_headers(cls, user) -> dict: + """Build authentication headers for CalDAV requests. + + Args: + user: Object with .email and .organization_id attributes. + + Raises: + ValueError: If user.email is not set. + """ + if not user.email: + raise ValueError("User has no email address") return { "X-Api-Key": cls.get_api_key(), - "X-Forwarded-User": email, + "X-Forwarded-User": user.email, + "X-CalDAV-Organization": str(user.organization_id), } def build_url(self, path: str, query: str = "") -> str: @@ -70,7 +82,7 @@ class CalDAVHTTPClient: def request( # noqa: PLR0913 # pylint: disable=too-many-arguments self, method: str, - email: str, + user, path: str, *, query: str = "", @@ -80,7 +92,7 @@ class CalDAVHTTPClient: content_type: str | None = None, ) -> requests.Response: """Make an authenticated HTTP request to the CalDAV server.""" - headers = self.build_base_headers(email) + headers = self.build_base_headers(user) if content_type: headers["Content-Type"] = content_type if extra_headers: @@ -95,9 +107,13 @@ class CalDAVHTTPClient: timeout=timeout or self.DEFAULT_TIMEOUT, ) - def get_dav_client(self, email: str) -> DAVClient: - """Return a configured caldav.DAVClient for the given user email.""" - headers = self.build_base_headers(email) + def get_dav_client(self, user) -> DAVClient: + """Return a configured caldav.DAVClient for the given user. + + Args: + user: Object with .email and .organization_id attributes. + """ + headers = self.build_base_headers(user) caldav_url = f"{self.base_url}{self.BASE_URI_PATH}/" return DAVClient( url=caldav_url, @@ -107,38 +123,58 @@ class CalDAVHTTPClient: headers=headers, ) - def find_event_by_uid(self, email: str, uid: str) -> tuple[str | None, str | None]: + def find_event_by_uid( + self, user, uid: str + ) -> tuple[str | None, str | None, str | None]: """Find an event by UID across all of the user's calendars. - Returns (ical_data, href) or (None, None). + Returns (ical_data, href, etag) or (None, None, None). """ - client = self.get_dav_client(email) + client = self.get_dav_client(user) try: principal = client.principal() for cal in principal.calendars(): try: event = cal.object_by_uid(uid) - return event.data, str(event.url.path) + etag = getattr(event, "props", {}).get("{DAV:}getetag") or getattr( + event, "etag", None + ) + return event.data, str(event.url.path), etag except caldav_lib.error.NotFoundError: continue - logger.warning("Event UID %s not found in user %s calendars", uid, email) - return None, None + logger.warning( + "Event UID %s not found in user %s calendars", uid, user.email + ) + return None, None, None except Exception: # pylint: disable=broad-exception-caught logger.exception("CalDAV error looking up event %s", uid) - return None, None + return None, None, None - def put_event(self, email: str, href: str, ical_data: str) -> bool: - """PUT updated iCalendar data back to CalDAV. Returns True on success.""" + def put_event( + self, user, href: str, ical_data: str, etag: str | None = None + ) -> bool: + """PUT updated iCalendar data back to CalDAV. Returns True on success. + + If *etag* is provided, the request includes an If-Match header to + prevent lost updates from concurrent modifications. + """ try: + extra_headers = {} + if etag: + extra_headers["If-Match"] = etag response = self.request( "PUT", - email, + user, href, data=ical_data.encode("utf-8"), content_type="text/calendar; charset=utf-8", + extra_headers=extra_headers or None, ) if response.status_code in (200, 201, 204): return True + if response.status_code == 412: + logger.warning("CalDAV PUT conflict (ETag mismatch) for %s", href) + return False logger.error( "CalDAV PUT failed: %s %s", response.status_code, @@ -160,10 +196,10 @@ class CalDAVHTTPClient: cal = icalendar.Calendar.from_ical(ical_data) updated = False + target = f"mailto:{email.lower()}" for component in cal.walk("VEVENT"): for _name, attendee in component.property_items("ATTENDEE"): - attendee_val = str(attendee).lower() - if email.lower() in attendee_val: + if str(attendee).lower().strip() == target: attendee.params["PARTSTAT"] = icalendar.vText(new_partstat) updated = True @@ -182,14 +218,19 @@ class CalDAVClient: self._http = CalDAVHTTPClient() self.base_url = self._http.base_url + def _calendar_url(self, calendar_path: str) -> str: + """Build a full URL for a calendar path, including the BASE_URI_PATH.""" + return f"{self.base_url}{CalDAVHTTPClient.BASE_URI_PATH}{calendar_path}" + def _get_client(self, user) -> DAVClient: """ Get a CalDAV client for the given user. The CalDAV server requires API key authentication via Authorization header and X-Forwarded-User header for user identification. + Includes X-CalDAV-Organization when the user has an org. """ - return self._http.get_dav_client(user.email) + return self._http.get_dav_client(user) def get_calendar_info(self, user, calendar_path: str) -> dict | None: """ @@ -197,7 +238,7 @@ class CalDAVClient: Returns dict with name, color, description or None if not found. """ client = self._get_client(user) - calendar_url = f"{self.base_url}{calendar_path}" + calendar_url = self._calendar_url(calendar_path) try: calendar = client.calendar(url=calendar_url) @@ -227,37 +268,53 @@ class CalDAVClient: logger.error("Failed to get calendar info from CalDAV: %s", str(e)) return None - def create_calendar( - self, user, calendar_name: str, calendar_id: str, color: str = "" + def create_calendar( # pylint: disable=too-many-arguments + self, + user, + calendar_name: str = "", + calendar_id: str = "", + color: str = "", + *, + name: str = "", ) -> str: """ Create a new calendar in CalDAV server for the given user. Returns the CalDAV server path for the calendar. """ + calendar_name = calendar_name or name + if not calendar_id: + calendar_id = str(uuid4()) + if not color: + color = settings.DEFAULT_CALENDAR_COLOR client = self._get_client(user) principal = client.principal() try: - # Create calendar using caldav library - calendar = principal.make_calendar(name=calendar_name) + # Pass cal_id so the library uses our UUID for the path. + calendar = principal.make_calendar(name=calendar_name, cal_id=calendar_id) - # Set calendar color if provided if color: calendar.set_properties([CalendarColor(color)]) - # CalDAV server calendar path format: /calendars/{username}/{calendar_id}/ - # The caldav library returns a URL object, convert to string and extract path + # Extract CalDAV-relative path from the calendar URL calendar_url = str(calendar.url) - # Extract path from full URL if calendar_url.startswith(self.base_url): path = calendar_url[len(self.base_url) :] else: - # Fallback: construct path manually based on standard CalDAV structure - # CalDAV servers typically create calendars under /calendars/{principal}/ - path = f"/calendars/{user.email}/{calendar_id}/" + path = f"/calendars/users/{user.email}/{calendar_id}/" + + base_prefix = CalDAVHTTPClient.BASE_URI_PATH + if path.startswith(base_prefix): + path = path[len(base_prefix) :] + if not path.startswith("/"): + path = "/" + path + + path = unquote(path) logger.info( - "Created calendar in CalDAV server: %s at %s", calendar_name, path + "Created calendar in CalDAV server: %s at %s", + calendar_name, + path, ) return path except Exception as e: @@ -285,7 +342,7 @@ class CalDAVClient: client = self._get_client(user) # Get calendar by URL - calendar_url = f"{self.base_url}{calendar_path}" + calendar_url = self._calendar_url(calendar_path) calendar = client.calendar(url=calendar_url) try: @@ -323,7 +380,7 @@ class CalDAVClient: Returns the event UID. """ client = self._get_client(user) - calendar_url = f"{self.base_url}{calendar_path}" + calendar_url = self._calendar_url(calendar_path) calendar = client.calendar(url=calendar_url) try: @@ -342,7 +399,7 @@ class CalDAVClient: """ client = self._get_client(user) - calendar_url = f"{self.base_url}{calendar_path}" + calendar_url = self._calendar_url(calendar_path) calendar = client.calendar(url=calendar_url) # Extract event data @@ -385,27 +442,11 @@ class CalDAVClient: """Update an existing event in CalDAV server.""" client = self._get_client(user) - calendar_url = f"{self.base_url}{calendar_path}" + calendar_url = self._calendar_url(calendar_path) calendar = client.calendar(url=calendar_url) try: - # Search for the event by UID - events = calendar.search(event=True) - target_event = None - - for event in events: - event_uid_value = None - if hasattr(event, "icalendar_component"): - event_uid_value = str(event.icalendar_component.get("uid", "")) - elif hasattr(event, "vobject_instance"): - event_uid_value = event.vobject_instance.vevent.uid.value - - if event_uid_value == event_uid: - target_event = event - break - - if not target_event: - raise ValueError(f"Event with UID {event_uid} not found") + target_event = calendar.object_by_uid(event_uid) # Update event properties dtstart = event_data.get("start") @@ -432,6 +473,8 @@ class CalDAVClient: target_event.save() logger.info("Updated event in CalDAV server: %s", event_uid) + except NotFoundError: + raise ValueError(f"Event with UID {event_uid} not found") from None except Exception as e: logger.error("Failed to update event in CalDAV server: %s", str(e)) raise @@ -440,36 +483,44 @@ class CalDAVClient: """Delete an event from CalDAV server.""" client = self._get_client(user) - calendar_url = f"{self.base_url}{calendar_path}" + calendar_url = self._calendar_url(calendar_path) calendar = client.calendar(url=calendar_url) try: - # Search for the event by UID - events = calendar.search(event=True) - target_event = None - - for event in events: - event_uid_value = None - if hasattr(event, "icalendar_component"): - event_uid_value = str(event.icalendar_component.get("uid", "")) - elif hasattr(event, "vobject_instance"): - event_uid_value = event.vobject_instance.vevent.uid.value - - if event_uid_value == event_uid: - target_event = event - break - - if not target_event: - raise ValueError(f"Event with UID {event_uid} not found") - - # Delete the event + target_event = calendar.object_by_uid(event_uid) target_event.delete() - logger.info("Deleted event from CalDAV server: %s", event_uid) + except NotFoundError: + raise ValueError(f"Event with UID {event_uid} not found") from None except Exception as e: logger.error("Failed to delete event from CalDAV server: %s", str(e)) raise + def get_user_calendar_paths(self, user) -> list[str]: + """Return a list of CalDAV-relative calendar paths for the user.""" + client = self._get_client(user) + principal = client.principal() + paths = [] + base = f"{self.base_url}{CalDAVHTTPClient.BASE_URI_PATH}" + for cal in principal.calendars(): + url = str(cal.url) + if url.startswith(base): + paths.append(unquote(url[len(base) :])) + return paths + + def create_default_calendar(self, user) -> str: + """Create a default calendar for a user. Returns the caldav_path.""" + from core.services.translation_service import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + TranslationService, + ) + + calendar_id = str(uuid4()) + lang = TranslationService.resolve_language(email=user.email) + calendar_name = TranslationService.t("calendar.list.defaultCalendarName", lang) + return self.create_calendar( + user, calendar_name, calendar_id, color=settings.DEFAULT_CALENDAR_COLOR + ) + def _parse_event(self, event) -> Optional[dict]: """ Parse a caldav Event object and return event data as dictionary. @@ -491,13 +542,15 @@ class CalDAVClient: # Convert datetime to string format for consistency if event_data["start"]: if isinstance(event_data["start"], datetime): - event_data["start"] = event_data["start"].strftime("%Y%m%dT%H%M%SZ") + utc_start = event_data["start"].astimezone(dt_timezone.utc) + event_data["start"] = utc_start.strftime("%Y%m%dT%H%M%SZ") elif isinstance(event_data["start"], date): event_data["start"] = event_data["start"].strftime("%Y%m%d") if event_data["end"]: if isinstance(event_data["end"], datetime): - event_data["end"] = event_data["end"].strftime("%Y%m%dT%H%M%SZ") + utc_end = event_data["end"].astimezone(dt_timezone.utc) + event_data["end"] = utc_end.strftime("%Y%m%dT%H%M%SZ") elif isinstance(event_data["end"], date): event_data["end"] = event_data["end"].strftime("%Y%m%d") @@ -507,60 +560,19 @@ class CalDAVClient: return None -class CalendarService: - """ - High-level service for managing calendars and events. - """ - - def __init__(self): - self.caldav = CalDAVClient() - - def create_default_calendar(self, user) -> str: - """Create a default calendar for a user. Returns the caldav_path.""" - from core.services.translation_service import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel - TranslationService, - ) - - calendar_id = str(uuid4()) - lang = TranslationService.resolve_language(email=user.email) - calendar_name = TranslationService.t("calendar.list.defaultCalendarName", lang) - return self.caldav.create_calendar( - user, calendar_name, calendar_id, color=settings.DEFAULT_CALENDAR_COLOR - ) - - def create_calendar(self, user, name: str, color: str = "") -> str: - """Create a new calendar for a user. Returns the caldav_path.""" - calendar_id = str(uuid4()) - return self.caldav.create_calendar( - user, name, calendar_id, color=color or settings.DEFAULT_CALENDAR_COLOR - ) - - def get_events(self, user, caldav_path: str, start=None, end=None) -> list: - """Get events from a calendar. Returns parsed event data.""" - return self.caldav.get_events(user, caldav_path, start, end) - - def create_event(self, user, caldav_path: str, event_data: dict) -> str: - """Create a new event.""" - return self.caldav.create_event(user, caldav_path, event_data) - - def update_event( - self, user, caldav_path: str, event_uid: str, event_data: dict - ) -> None: - """Update an existing event.""" - self.caldav.update_event(user, caldav_path, event_uid, event_data) - - def delete_event(self, user, caldav_path: str, event_uid: str) -> None: - """Delete an event.""" - self.caldav.delete_event(user, caldav_path, event_uid) +# CalendarService is kept as an alias for backwards compatibility +# with tests and signals that reference it. +CalendarService = CalDAVClient # --------------------------------------------------------------------------- # CalDAV path utilities # --------------------------------------------------------------------------- -# Pattern: /calendars/// +# Pattern: /calendars/users/// +# or /calendars/resources/// CALDAV_PATH_PATTERN = re.compile( - r"^/calendars/[^/]+/[a-zA-Z0-9-]+/$", + r"^/calendars/(users|resources)/[^/]+/[a-zA-Z0-9-]+/$", ) @@ -568,8 +580,8 @@ def normalize_caldav_path(caldav_path): """Normalize CalDAV path to consistent format. Strips the CalDAV API prefix (e.g. /api/v1.0/caldav/) if present, - so that paths like /api/v1.0/caldav/calendars/user@ex.com/uuid/ - become /calendars/user@ex.com/uuid/. + so that paths like /api/v1.0/caldav/calendars/users/user@ex.com/uuid/ + become /calendars/users/user@ex.com/uuid/. """ if not caldav_path.startswith("/"): caldav_path = "/" + caldav_path @@ -582,19 +594,60 @@ def normalize_caldav_path(caldav_path): return caldav_path +def _resource_belongs_to_org(resource_id: str, org_id: str) -> bool: + """Check whether a resource principal belongs to the given organization. + + Queries the CalDAV internal API. Returns False on any error (fail-closed). + """ + api_key = settings.CALDAV_INTERNAL_API_KEY + caldav_url = settings.CALDAV_URL + if not api_key or not caldav_url: + return False + try: + resp = requests.get( + f"{caldav_url.rstrip('/')}/caldav/internal-api/resources/{resource_id}", + headers={"X-Internal-Api-Key": api_key}, + timeout=10, + ) + if resp.status_code != 200: + return False + data = resp.json() + return data.get("org_id") == org_id + except Exception: # pylint: disable=broad-exception-caught + logger.exception("Failed to verify resource org for %s", resource_id) + return False + + def verify_caldav_access(user, caldav_path): """Verify that the user has access to the CalDAV calendar. Checks that: 1. The path matches the expected pattern (prevents path injection) - 2. The user's email matches the email in the path + 2. For user calendars: the user's email matches the email in the path + 3. For resource calendars: the user has an organization + + Note: Fine-grained org-to-resource authorization is enforced by SabreDAV + itself (via X-CalDAV-Organization header). This check only gates access + for Django-level features (subscription tokens, imports). """ if not CALDAV_PATH_PATTERN.match(caldav_path): return False parts = caldav_path.strip("/").split("/") - if len(parts) >= 2 and parts[0] == "calendars": - path_email = unquote(parts[1]) + if len(parts) < 3 or parts[0] != "calendars": + return False + # User calendars: calendars/users// + if parts[1] == "users": + if not user.email: + return False + path_email = unquote(parts[2]) return path_email.lower() == user.email.lower() + # Resource calendars: calendars/resources// + # Org membership is required. Fine-grained org-to-resource authorization + # is enforced by SabreDAV via the X-CalDAV-Organization header on every + # proxied request. For subscription tokens / imports, callers should + # additionally use _resource_belongs_to_org() to verify ownership. + if parts[1] == "resources": + return bool(getattr(user, "organization_id", None)) return False @@ -605,10 +658,16 @@ def validate_caldav_proxy_path(path): - Directory traversal sequences (../) - Null bytes - Paths that don't start with expected prefixes + + URL-decodes the path first so that encoded payloads like + ``%2e%2e`` or ``%00`` cannot bypass the checks. """ if not path: return True # Empty path is fine (root request) + # Decode percent-encoded characters before validation + path = unquote(path) + # Block directory traversal if ".." in path: return False @@ -617,10 +676,60 @@ def validate_caldav_proxy_path(path): if "\x00" in path: return False + clean = path.lstrip("/") + + # Explicitly block internal-api/ paths — these must never be proxied. + # The allowlist below already rejects them, but an explicit block makes + # the intent clear and survives future allowlist additions. + blocked_prefixes = ("internal-api/",) + if clean and any(clean.startswith(prefix) for prefix in blocked_prefixes): + return False + # Path must start with a known CalDAV resource prefix allowed_prefixes = ("calendars/", "principals/", ".well-known/") - clean = path.lstrip("/") if clean and not any(clean.startswith(prefix) for prefix in allowed_prefixes): return False return True + + +def cleanup_organization_caldav_data(org): + """Clean up CalDAV data for all members of an organization. + + Deletes each member's CalDAV data via the SabreDAV internal API, + then deletes the Django User objects so the PROTECT foreign key + on User.organization doesn't block org deletion. + + Called from Organization.delete() — NOT a signal, because the + PROTECT FK raises ProtectedError before pre_delete fires. + """ + if not settings.CALDAV_INTERNAL_API_KEY: + return + + http = CalDAVHTTPClient() + members = list(org.members.all()) + + for user in members: + if not user.email: + continue + try: + http.request( + "POST", + user, + "internal-api/users/delete", + data=json.dumps({"email": user.email}).encode("utf-8"), + content_type="application/json", + extra_headers={ + "X-Internal-Api-Key": settings.CALDAV_INTERNAL_API_KEY, + }, + ) + except Exception: # pylint: disable=broad-exception-caught + logger.exception( + "Failed to clean up CalDAV data for user %s (org %s)", + user.email, + org.external_id, + ) + + # Delete all members so the PROTECT FK doesn't block org deletion. + # CalDAV cleanup is best-effort; orphaned CalDAV data is acceptable. + org.members.all().delete() diff --git a/src/backend/core/services/calendar_invitation_service.py b/src/backend/core/services/calendar_invitation_service.py index b370086..84d54bb 100644 --- a/src/backend/core/services/calendar_invitation_service.py +++ b/src/backend/core/services/calendar_invitation_service.py @@ -20,7 +20,7 @@ from urllib.parse import urlencode from django.conf import settings from django.core.mail import EmailMultiAlternatives -from django.core.signing import Signer +from django.core.signing import TimestampSigner from django.template.loader import render_to_string from core.services.translation_service import TranslationService @@ -424,8 +424,8 @@ class CalendarInvitationService: # pylint: disable=too-many-instance-attributes "time_str": time_str, "is_update": event.sequence > 0, "is_cancel": method == self.METHOD_CANCEL, - "app_name": getattr(settings, "APP_NAME", "Calendrier"), - "app_url": getattr(settings, "APP_URL", ""), + "app_name": settings.APP_NAME, + "app_url": settings.APP_URL, # Translated content blocks "content": { "title": t(f"email.{type_key}.title", lang), @@ -457,13 +457,13 @@ class CalendarInvitationService: # pylint: disable=too-many-instance-attributes "footer": t( f"email.footer.{'invitation' if type_key == 'invitation' else 'notification'}", lang, - appName=getattr(settings, "APP_NAME", "Calendrier"), + appName=settings.APP_NAME, ), } # Add RSVP links for REQUEST method (invitations and updates) if method == self.METHOD_REQUEST: - signer = Signer(salt="rsvp") + signer = TimestampSigner(salt="rsvp") # Strip mailto: prefix (case-insensitive) for shorter tokens organizer = re.sub( r"^mailto:", "", event.organizer_email, flags=re.IGNORECASE @@ -475,7 +475,7 @@ class CalendarInvitationService: # pylint: disable=too-many-instance-attributes "organizer": organizer, } ) - app_url = getattr(settings, "APP_URL", "") + app_url = settings.APP_URL base = f"{app_url}/rsvp/" for action in ("accept", "tentative", "decline"): partstat = { @@ -498,7 +498,7 @@ class CalendarInvitationService: # pylint: disable=too-many-instance-attributes When False (default), strips METHOD so the ICS is treated as a plain calendar object — our own RSVP web links handle responses instead. """ - itip_enabled = getattr(settings, "CALENDAR_ITIP_ENABLED", False) + itip_enabled = settings.CALENDAR_ITIP_ENABLED if itip_enabled: if "METHOD:" not in icalendar_data.upper(): @@ -549,10 +549,8 @@ class CalendarInvitationService: # pylint: disable=too-many-instance-attributes """ try: # Get email settings - from_addr = getattr( - settings, - "CALENDAR_INVITATION_FROM_EMAIL", - getattr(settings, "DEFAULT_FROM_EMAIL", "noreply@example.com"), + from_addr = ( + settings.CALENDAR_INVITATION_FROM_EMAIL or settings.DEFAULT_FROM_EMAIL ) # Create the email message @@ -571,7 +569,7 @@ class CalendarInvitationService: # pylint: disable=too-many-instance-attributes ics_attachment = MIMEBase("text", "calendar") ics_attachment.set_payload(ics_content.encode("utf-8")) encoders.encode_base64(ics_attachment) - itip_enabled = getattr(settings, "CALENDAR_ITIP_ENABLED", False) + itip_enabled = settings.CALENDAR_ITIP_ENABLED content_type = "text/calendar; charset=utf-8" if itip_enabled: content_type += f"; method={ics_method}" diff --git a/src/backend/core/services/import_service.py b/src/backend/core/services/import_service.py index aed11a0..a47699a 100644 --- a/src/backend/core/services/import_service.py +++ b/src/backend/core/services/import_service.py @@ -3,6 +3,8 @@ import logging from dataclasses import dataclass, field +from django.conf import settings + import requests from core.services.caldav_service import CalDAVHTTPClient @@ -41,37 +43,43 @@ class ICSImportService: def import_events(self, user, caldav_path: str, ics_data: bytes) -> ImportResult: """Import events from ICS data into a calendar. - Sends the raw ICS bytes to SabreDAV's ?import endpoint which - handles all ICS parsing, splitting by UID, VALARM repair, and - per-event insertion. + Sends the raw ICS bytes to the SabreDAV internal API import + endpoint which handles all ICS parsing, splitting by UID, + VALARM repair, and per-event insertion. Args: user: The authenticated user performing the import. caldav_path: CalDAV path of the calendar - (e.g. /calendars/user@example.com/uuid/). + (e.g. /calendars/users/user@example.com/uuid/). ics_data: Raw ICS file content. """ result = ImportResult() - try: - api_key = CalDAVHTTPClient.get_api_key() - except ValueError: - result.errors.append("CALDAV_OUTBOUND_API_KEY is not configured") + api_key = settings.CALDAV_INTERNAL_API_KEY + if not api_key: + result.errors.append("CALDAV_INTERNAL_API_KEY is not configured") return result - # Timeout scales with file size: 60s base + 30s per MB of ICS data. - # 8000 events (~4MB) took ~70s in practice. - timeout = 60 + int(len(ics_data) / 1024 / 1024) * 30 + # Extract calendar URI from caldav_path + # Path format: /calendars/users/// + parts = caldav_path.strip("/").split("/") + if len(parts) == 4 and parts[0] == "calendars" and parts[1] == "users": + principal_user = parts[2] + calendar_uri = parts[3] + else: + result.errors.append("Invalid calendar path") + return result + # import runs in a background task so we can wait a decent amount of time + timeout = 1200 # 20 minutes try: response = self._http.request( "POST", - user.email, - caldav_path, - query="import", + user, + f"internal-api/import/{principal_user}/{calendar_uri}", data=ics_data, content_type="text/calendar", - extra_headers={"X-Calendars-Import": api_key}, + extra_headers={"X-Internal-Api-Key": api_key}, timeout=timeout, ) except requests.RequestException as exc: diff --git a/src/backend/core/services/resource_service.py b/src/backend/core/services/resource_service.py new file mode 100644 index 0000000..2c73c2c --- /dev/null +++ b/src/backend/core/services/resource_service.py @@ -0,0 +1,182 @@ +"""Service for managing calendar resource provisioning via CalDAV.""" + +import json +import logging +from uuid import UUID, uuid4 + +from django.conf import settings + +from core.services.caldav_service import CalDAVHTTPClient + +logger = logging.getLogger(__name__) + + +class ResourceProvisioningError(Exception): + """Raised when resource provisioning fails.""" + + +class ResourceService: + """Provisions and deletes resource principals in SabreDAV. + + Resources are CalDAV principals — this service creates them by + making HTTP requests to the SabreDAV internal API. No Django model + is created; the CalDAV principal IS the resource. + """ + + def __init__(self): + self._http = CalDAVHTTPClient() + + def _resource_email(self, resource_id): + """Generate a resource scheduling address.""" + domain = settings.RESOURCE_EMAIL_DOMAIN + if not domain: + domain = "resource.invalid" + return f"{resource_id}@{domain}" + + def create_resource(self, user, name, resource_type="ROOM"): + """Provision a resource principal and its default calendar. + + Args: + user: The admin user creating the resource (provides auth context). + name: Display name for the resource. + resource_type: "ROOM" or "RESOURCE". + + Returns: + dict with resource info: id, email, principal_uri, calendar_uri. + + Raises: + ResourceProvisioningError on failure. + """ + if resource_type not in ("ROOM", "RESOURCE"): + raise ResourceProvisioningError( + "resource_type must be 'ROOM' or 'RESOURCE'." + ) + + if not settings.CALDAV_INTERNAL_API_KEY: + raise ResourceProvisioningError( + "CALDAV_INTERNAL_API_KEY is not configured." + ) + + resource_id = str(uuid4()) + email = self._resource_email(resource_id) + org_id = str(user.organization_id) + + try: + response = self._http.request( + "POST", + user, + "internal-api/resources/", + data=self._json_bytes( + { + "resource_id": resource_id, + "name": name, + "email": email, + "resource_type": resource_type, + "org_id": org_id, + } + ), + content_type="application/json", + extra_headers={ + "X-Internal-Api-Key": settings.CALDAV_INTERNAL_API_KEY, + }, + ) + except Exception as e: + logger.error("Failed to create resource principal: %s", e) + raise ResourceProvisioningError( + "Failed to create resource principal." + ) from e + + if response.status_code == 409: + raise ResourceProvisioningError(f"Resource '{resource_id}' already exists.") + + if response.status_code != 201: + logger.error( + "InternalApi create resource returned %s: %s", + response.status_code, + response.text[:500], + ) + raise ResourceProvisioningError("Failed to create resource principal.") + + principal_uri = f"principals/resources/{resource_id}" + calendar_uri = f"calendars/resources/{resource_id}/default/" + + return { + "id": resource_id, + "email": email, + "name": name, + "resource_type": resource_type, + "principal_uri": principal_uri, + "calendar_uri": calendar_uri, + } + + @staticmethod + def _validate_resource_id(resource_id): + """Validate that resource_id is a proper UUID. + + Raises ResourceProvisioningError if the ID is not a valid UUID, + preventing path traversal via crafted IDs. + """ + try: + UUID(str(resource_id)) + except (ValueError, AttributeError) as e: + raise ResourceProvisioningError( + "Invalid resource ID: must be a valid UUID." + ) from e + + def delete_resource(self, user, resource_id): + """Delete a resource principal and its calendar. + + Events in user calendars that reference this resource are left + as-is — the resource address becomes unresolvable. + + Args: + user: The admin user requesting deletion. + resource_id: The resource UUID. + + Raises: + ResourceProvisioningError on failure. + """ + self._validate_resource_id(resource_id) + + if not settings.CALDAV_INTERNAL_API_KEY: + raise ResourceProvisioningError( + "CALDAV_INTERNAL_API_KEY is not configured." + ) + + try: + response = self._http.request( + "DELETE", + user, + f"internal-api/resources/{resource_id}", + extra_headers={ + "X-Internal-Api-Key": settings.CALDAV_INTERNAL_API_KEY, + }, + ) + except Exception as e: + logger.error("Failed to delete resource: %s", e) + raise ResourceProvisioningError("Failed to delete resource.") from e + + if response.status_code == 404: + raise ResourceProvisioningError(f"Resource '{resource_id}' not found.") + + if response.status_code == 403: + try: + error_msg = response.json().get("error", "") + except ValueError: + error_msg = "" + raise ResourceProvisioningError( + error_msg or "Cannot delete a resource from a different organization." + ) + + if response.status_code not in (200, 204): + logger.error( + "InternalApi delete resource returned %s: %s", + response.status_code, + response.text[:500], + ) + raise ResourceProvisioningError("Failed to delete resource.") + + @staticmethod + def _json_bytes(data): + """Serialize a dict to JSON bytes.""" + return json.dumps(data).encode("utf-8") diff --git a/src/backend/core/services/translation_service.py b/src/backend/core/services/translation_service.py index ddee4c2..6aa8ad8 100644 --- a/src/backend/core/services/translation_service.py +++ b/src/backend/core/services/translation_service.py @@ -2,6 +2,7 @@ import json import logging +import threading from datetime import datetime from typing import Optional @@ -40,6 +41,7 @@ class TranslationService: """Lightweight translation service backed by translations.json.""" _translations = None + _load_lock = threading.Lock() @classmethod def _load(cls): @@ -47,12 +49,17 @@ class TranslationService: if cls._translations is not None: return - path = getattr(settings, "TRANSLATIONS_JSON_PATH", "") - if not path: - raise RuntimeError("TRANSLATIONS_JSON_PATH setting is not configured") + with cls._load_lock: + # Double-check after acquiring lock + if cls._translations is not None: + return - with open(path, encoding="utf-8") as f: - cls._translations = json.load(f) + path = settings.TRANSLATIONS_JSON_PATH + if not path: + raise RuntimeError("TRANSLATIONS_JSON_PATH setting is not configured") + + with open(path, encoding="utf-8") as f: + cls._translations = json.load(f) @classmethod def _get_nested(cls, data: dict, dotted_key: str): @@ -104,15 +111,15 @@ class TranslationService: if email: try: - from core.models import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel - User, + from django.contrib.auth import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + get_user_model, ) - user = User.objects.filter(email=email).first() + user = get_user_model().objects.filter(email=email).first() if user and user.language: return cls.normalize_lang(user.language) except Exception: # pylint: disable=broad-exception-caught - logger.exception("Failed to resolve language for email %s", email) + logger.exception("Failed to resolve language for recipient") return "fr" diff --git a/src/backend/core/signals.py b/src/backend/core/signals.py index e7008cd..c80d7cd 100644 --- a/src/backend/core/signals.py +++ b/src/backend/core/signals.py @@ -2,15 +2,17 @@ Declare and configure the signals for the calendars core application """ +import json import logging from django.conf import settings from django.contrib.auth import get_user_model -from django.db.models.signals import post_save +from django.db import transaction +from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from core.entitlements import EntitlementsUnavailableError, get_user_entitlements -from core.services.caldav_service import CalendarService +from core.services.caldav_service import CalDAVHTTPClient, CalendarService logger = logging.getLogger(__name__) User = get_user_model() @@ -32,7 +34,7 @@ def provision_default_calendar(sender, instance, created, **kwargs): # pylint: # never create a calendar if we can't confirm access. try: entitlements = get_user_entitlements(instance.sub, instance.email) - if not entitlements.get("can_access", True): + if not entitlements.get("can_access", False): logger.info( "Skipped calendar creation for %s (not entitled)", instance.email, @@ -48,22 +50,45 @@ def provision_default_calendar(sender, instance, created, **kwargs): # pylint: try: service = CalendarService() service.create_default_calendar(instance) - logger.info("Created default calendar for user %s", instance.email) - except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught - # In tests, CalDAV server may not be available, so fail silently - # Check if it's a database error that suggests we're in tests - error_str = str(e).lower() - if "does not exist" in error_str or "relation" in error_str: - # Likely in test environment, fail silently - logger.debug( - "Skipped calendar creation for user %s (likely test environment): %s", - instance.email, - str(e), + logger.info("Created default calendar for user %s", instance.pk) + except Exception: # pylint: disable=broad-exception-caught + logger.exception( + "Failed to create default calendar for user %s", + instance.pk, + ) + + +@receiver(pre_delete, sender=User) +def delete_user_caldav_data(sender, instance, **kwargs): # pylint: disable=unused-argument + """Schedule CalDAV data cleanup when a user is deleted. + + Uses on_commit so the external CalDAV call only fires after + the DB transaction commits — avoids orphaned state on rollback. + """ + email = instance.email + if not email: + return + + if not settings.CALDAV_INTERNAL_API_KEY: + return + + api_key = settings.CALDAV_INTERNAL_API_KEY + + def _cleanup(): + try: + http = CalDAVHTTPClient() + http.request( + "POST", + instance, + "internal-api/users/delete", + data=json.dumps({"email": email}).encode("utf-8"), + content_type="application/json", + extra_headers={"X-Internal-Api-Key": api_key}, ) - else: - # Real error, log it - logger.error( - "Failed to create default calendar for user %s: %s", - instance.email, - str(e), + except Exception: # pylint: disable=broad-exception-caught + logger.exception( + "Failed to clean up CalDAV data for user %s", + email, ) + + transaction.on_commit(_cleanup) diff --git a/src/backend/core/task_utils.py b/src/backend/core/task_utils.py new file mode 100644 index 0000000..765d4a5 --- /dev/null +++ b/src/backend/core/task_utils.py @@ -0,0 +1,170 @@ +"""Task queue utilities. + +Provides decorators and helpers that abstract away the underlying task +queue library (currently Dramatiq). Application code should import from +here instead of importing dramatiq directly. +""" + +import json +import logging +from typing import Any, Optional + +from django.core.cache import cache +from django.utils import timezone + +import dramatiq +from dramatiq.brokers.stub import StubBroker +from dramatiq.middleware import CurrentMessage + +logger = logging.getLogger(__name__) + +TASK_PROGRESS_CACHE_TIMEOUT = 86400 # 24 hours +TASK_TRACKING_CACHE_TTL = 86400 * 30 # 30 days + + +# --------------------------------------------------------------------------- +# Task wrapper (Celery-compatible API) +# --------------------------------------------------------------------------- + + +class Task: + """Wrapper around a Dramatiq Message with a Celery-like API.""" + + def __init__(self, message): + self._message = message + + @property + def id(self): + """Celery-compatible task ID (maps to message_id).""" + return self._message.message_id + + def track_owner(self, user_id): + """Register tracking metadata for permission checks.""" + cache.set( + f"task_tracking:{self.id}", + json.dumps( + { + "owner": str(user_id), + "actor_name": self._message.actor_name, + "queue_name": self._message.queue_name, + } + ), + timeout=TASK_TRACKING_CACHE_TTL, + ) + + def __getattr__(self, name): + return getattr(self._message, name) + + +class CeleryCompatActor(dramatiq.Actor): + """Actor subclass that adds a .delay() method returning a Task.""" + + def delay(self, *args, **kwargs): + """Dispatch the task asynchronously, returning a Task wrapper.""" + message = self.send(*args, **kwargs) + return Task(message) + + +# --------------------------------------------------------------------------- +# Decorators +# --------------------------------------------------------------------------- + + +def register_task(*args, **kwargs): + """Decorator to register a task (wraps dramatiq.actor). + + Usage:: + + @register_task(queue="import") + def my_task(arg): + ... + """ + kwargs.setdefault("store_results", True) + if "queue" in kwargs: + kwargs.setdefault("queue_name", kwargs.pop("queue")) + kwargs.setdefault("actor_class", CeleryCompatActor) + + def decorator(fn): + return dramatiq.actor(fn, **kwargs) + + if args and callable(args[0]): + return decorator(args[0]) + return decorator + + +# --------------------------------------------------------------------------- +# Task tracking & progress +# --------------------------------------------------------------------------- + + +def get_task_tracking(task_id: str) -> Optional[dict]: + """Get tracking metadata for a task, or None if not found.""" + raw = cache.get(f"task_tracking:{task_id}") + if raw is None: + return None + return json.loads(raw) + + +def set_task_progress(progress: int, metadata: Optional[dict[str, Any]] = None) -> None: + """Set the progress of the currently executing task.""" + current_message = CurrentMessage.get_current_message() + if not current_message: + logger.warning("set_task_progress called outside of a task") + return + + task_id = current_message.message_id + try: + progress = max(0, min(100, int(progress))) + except (TypeError, ValueError): + progress = 0 + + cache.set( + f"task_progress:{task_id}", + { + "progress": progress, + "timestamp": timezone.now().timestamp(), + "metadata": metadata or {}, + }, + timeout=TASK_PROGRESS_CACHE_TIMEOUT, + ) + + +def get_task_progress(task_id: str) -> Optional[dict[str, Any]]: + """Get the progress of a task by ID.""" + return cache.get(f"task_progress:{task_id}") + + +# --------------------------------------------------------------------------- +# EagerBroker for tests +# --------------------------------------------------------------------------- + + +class EagerBroker(StubBroker): + """Broker that executes tasks synchronously (for tests). + + Equivalent to Celery's CELERY_TASK_ALWAYS_EAGER mode. + Only runs CurrentMessage and Results middleware. + """ + + def enqueue(self, message, *, delay=None): + from dramatiq.results import Results # noqa: PLC0415 # pylint: disable=C0415 + + actor = self.get_actor(message.actor_name) + cm = next( + (m for m in self.middleware if isinstance(m, CurrentMessage)), + None, + ) + rm = next((m for m in self.middleware if isinstance(m, Results)), None) + prev = CurrentMessage.get_current_message() if cm else None + if cm: + cm.before_process_message(self, message) + try: + result = actor.fn(*message.args, **message.kwargs) + if rm: + rm.after_process_message(self, message, result=result) + finally: + if cm: + cm.after_process_message(self, message) + if prev is not None: + cm.before_process_message(self, prev) + return message diff --git a/src/backend/core/tasks.py b/src/backend/core/tasks.py new file mode 100644 index 0000000..480628c --- /dev/null +++ b/src/backend/core/tasks.py @@ -0,0 +1,50 @@ +"""Background tasks for the calendars core application.""" + +# pylint: disable=import-outside-toplevel + +import logging +from dataclasses import asdict + +from core.services.import_service import ICSImportService +from core.task_utils import register_task, set_task_progress + +logger = logging.getLogger(__name__) + + +@register_task(queue="import") +def import_events_task(user_id, caldav_path, ics_data_hex): + """Import events from ICS data in the background. + + Parameters are kept JSON-serialisable: + - user_id: pk of the User who triggered the import + - caldav_path: target CalDAV calendar path + - ics_data_hex: ICS bytes encoded as hex string + """ + from core.models import User # noqa: PLC0415 + + set_task_progress(0, {"message": "Starting import..."}) + + try: + user = User.objects.get(pk=user_id) + except User.DoesNotExist: + logger.error("import_events_task: user %s not found", user_id) + return { + "status": "FAILURE", + "result": None, + "error": "User not found", + } + + ics_data = bytes.fromhex(ics_data_hex) + set_task_progress(10, {"message": "Sending to CalDAV server..."}) + + service = ICSImportService() + result = service.import_events(user, caldav_path, ics_data) + + set_task_progress(100, {"message": "Import complete"}) + + result_dict = asdict(result) + return { + "status": "SUCCESS", + "result": result_dict, + "error": None, + } diff --git a/src/backend/core/templates/rsvp/confirm.html b/src/backend/core/templates/rsvp/confirm.html new file mode 100644 index 0000000..5fe1f9f --- /dev/null +++ b/src/backend/core/templates/rsvp/confirm.html @@ -0,0 +1,71 @@ + + + + + + {{ page_title }} + + + +
+
{{ status_icon|safe }}
+

{{ heading }}

+
+ + + +

...

+
+
+ + + diff --git a/src/backend/core/tests/authentication/test_backends.py b/src/backend/core/tests/authentication/test_backends.py index 7d45da5..c72ef9d 100644 --- a/src/backend/core/tests/authentication/test_backends.py +++ b/src/backend/core/tests/authentication/test_backends.py @@ -1,7 +1,7 @@ """Unit tests for the Authentication Backends.""" -import random import re +from unittest import mock from django.core.exceptions import SuspiciousOperation from django.test.utils import override_settings @@ -17,7 +17,14 @@ from core.factories import UserFactory pytestmark = pytest.mark.django_db +# Patch org resolution out by default in this module. +# Tests for org resolution are in test_organizations.py. +_no_org_resolve = mock.patch( + "core.authentication.backends.resolve_organization", lambda *a, **kw: None +) + +@_no_org_resolve def test_authentication_getter_existing_user_no_email( django_assert_num_queries, monkeypatch ): @@ -41,6 +48,7 @@ def test_authentication_getter_existing_user_no_email( assert user == db_user +@_no_org_resolve def test_authentication_getter_existing_user_via_email( django_assert_num_queries, monkeypatch ): @@ -57,7 +65,7 @@ def test_authentication_getter_existing_user_via_email( monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) - with django_assert_num_queries(4): # user by sub, user by mail, update sub + with django_assert_num_queries(5): # user by sub, user by mail, update sub, org user = klass.get_or_create_user( access_token="test-token", id_token=None, payload=None ) @@ -65,30 +73,24 @@ def test_authentication_getter_existing_user_via_email( assert user == db_user -def test_authentication_getter_email_none(monkeypatch): +def test_authentication_getter_email_none_rejected(monkeypatch): """ - If no user is found with the sub and no email is provided, a new user should be created. + If no user is found with the sub and no email is provided, + user creation is rejected (organization requires email domain). """ klass = OIDCAuthenticationBackend() - db_user = UserFactory(email=None) + UserFactory() # existing user with different sub def get_userinfo_mocked(*args): - user_info = {"sub": "123"} - if random.choice([True, False]): - user_info["email"] = None - return user_info + return {"sub": "123"} monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) - user = klass.get_or_create_user( - access_token="test-token", id_token=None, payload=None - ) - - # Since the sub and email didn't match, it should create a new user - assert models.User.objects.count() == 2 - assert user != db_user - assert user.sub == "123" + with pytest.raises( + SuspiciousOperation, match="Cannot create user without an organization" + ): + klass.get_or_create_user(access_token="test-token", id_token=None, payload=None) def test_authentication_getter_existing_user_no_fallback_to_email_allow_duplicate( @@ -154,6 +156,7 @@ def test_authentication_getter_existing_user_no_fallback_to_email_no_duplicate( assert models.User.objects.count() == 1 +@_no_org_resolve def test_authentication_getter_existing_user_with_email( django_assert_num_queries, monkeypatch ): @@ -161,7 +164,7 @@ def test_authentication_getter_existing_user_with_email( When the user's info contains an email and targets an existing user, """ klass = OIDCAuthenticationBackend() - user = UserFactory(full_name="John Doe", short_name="John") + user = UserFactory(full_name="John Doe") def get_userinfo_mocked(*args): return { @@ -182,6 +185,7 @@ def test_authentication_getter_existing_user_with_email( assert user == authenticated_user +@_no_org_resolve @pytest.mark.parametrize( "first_name, last_name, email", [ @@ -199,9 +203,7 @@ def test_authentication_getter_existing_user_change_fields_sub( and the user was identified by its "sub". """ klass = OIDCAuthenticationBackend() - user = UserFactory( - full_name="John Doe", short_name="John", email="john.doe@example.com" - ) + user = UserFactory(full_name="John Doe", email="john.doe@example.com") def get_userinfo_mocked(*args): return { @@ -213,8 +215,7 @@ def test_authentication_getter_existing_user_change_fields_sub( monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) - # One and only one additional update query when a field has changed - with django_assert_num_queries(3): + with django_assert_num_queries(4): authenticated_user = klass.get_or_create_user( access_token="test-token", id_token=None, payload=None ) @@ -223,9 +224,9 @@ def test_authentication_getter_existing_user_change_fields_sub( user.refresh_from_db() assert user.email == email assert user.full_name == f"{first_name:s} {last_name:s}" - assert user.short_name == first_name +@_no_org_resolve @pytest.mark.parametrize( "first_name, last_name, email", [ @@ -241,9 +242,7 @@ def test_authentication_getter_existing_user_change_fields_email( and the user was identified by its "email" as fallback. """ klass = OIDCAuthenticationBackend() - user = UserFactory( - full_name="John Doe", short_name="John", email="john.doe@example.com" - ) + user = UserFactory(full_name="John Doe", email="john.doe@example.com") def get_userinfo_mocked(*args): return { @@ -255,8 +254,7 @@ def test_authentication_getter_existing_user_change_fields_email( monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) - # One and only one additional update query when a field has changed - with django_assert_num_queries(4): + with django_assert_num_queries(5): authenticated_user = klass.get_or_create_user( access_token="test-token", id_token=None, payload=None ) @@ -265,13 +263,12 @@ def test_authentication_getter_existing_user_change_fields_email( user.refresh_from_db() assert user.email == email assert user.full_name == f"{first_name:s} {last_name:s}" - assert user.short_name == first_name -def test_authentication_getter_new_user_no_email(monkeypatch): +def test_authentication_getter_new_user_no_email_rejected(monkeypatch): """ - If no user matches the user's info sub, a user should be created. - User's info doesn't contain an email, created user's email should be empty. + If no user matches the sub and no email is provided, + user creation is rejected (organization requires email domain). """ klass = OIDCAuthenticationBackend() @@ -280,16 +277,10 @@ def test_authentication_getter_new_user_no_email(monkeypatch): monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked) - user = klass.get_or_create_user( - access_token="test-token", id_token=None, payload=None - ) - - assert user.sub == "123" - assert user.email is None - assert user.full_name is None - assert user.short_name is None - assert user.has_usable_password() is False - assert models.User.objects.count() == 1 + with pytest.raises( + SuspiciousOperation, match="Cannot create user without an organization" + ): + klass.get_or_create_user(access_token="test-token", id_token=None, payload=None) def test_authentication_getter_new_user_with_email(monkeypatch): @@ -314,7 +305,7 @@ def test_authentication_getter_new_user_with_email(monkeypatch): assert user.sub == "123" assert user.email == email assert user.full_name == "John Doe" - assert user.short_name == "John" + assert user.has_usable_password() is False assert models.User.objects.count() == 1 @@ -458,6 +449,7 @@ def test_authentication_getter_existing_disabled_user_via_email( assert models.User.objects.count() == 1 +@_no_org_resolve @responses.activate def test_authentication_session_tokens( django_assert_num_queries, monkeypatch, rf, settings @@ -498,7 +490,7 @@ def test_authentication_session_tokens( status=200, ) - with django_assert_num_queries(6): + with django_assert_num_queries(12): user = klass.authenticate( request, code="test-code", @@ -538,7 +530,7 @@ def test_authentication_store_claims_new_user(monkeypatch): assert user.sub == "123" assert user.email == email assert user.full_name == "John Doe" - assert user.short_name == "John" + assert user.has_usable_password() is False assert user.claims == {"iss": "https://example.com"} assert models.User.objects.count() == 1 diff --git a/src/backend/core/tests/conftest.py b/src/backend/core/tests/conftest.py index 17d0bef..a2a69d4 100644 --- a/src/backend/core/tests/conftest.py +++ b/src/backend/core/tests/conftest.py @@ -1,10 +1,9 @@ """Fixtures for tests in the calendars core application""" import base64 -from unittest import mock +from django.conf import settings from django.core.cache import cache -from django.db import connection import pytest import responses @@ -13,43 +12,86 @@ from core import factories from core.tests.utils.urls import reload_urls USER = "user" -TEAM = "team" -VIA = [USER, TEAM] + + +def _has_caldav_marker(request): + """Check if the test has the xdist_group('caldav') marker.""" + marker = request.node.get_closest_marker("xdist_group") + return marker is not None and marker.args and marker.args[0] == "caldav" @pytest.fixture(autouse=True) -def truncate_caldav_tables(django_db_setup, django_db_blocker): # pylint: disable=unused-argument - """Fixture to truncate CalDAV server tables at the start of each test. +def truncate_caldav_tables(request, django_db_setup, django_db_blocker): # pylint: disable=unused-argument + """Truncate CalDAV tables before each CalDAV E2E test. - CalDAV server tables are created by the CalDAV server container migrations, not Django. - We just truncate them to ensure clean state for each test. + Only runs for tests marked with @pytest.mark.xdist_group("caldav"). + Non-CalDAV tests don't touch the SabreDAV database, so truncating + from their worker would corrupt state for CalDAV tests running + concurrently on another xdist worker. """ - with django_db_blocker.unblock(): - with connection.cursor() as cursor: - # Truncate CalDAV server tables if they exist (created by CalDAV server container) - cursor.execute(""" - DO $$ - BEGIN - IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'principals') THEN - TRUNCATE TABLE principals CASCADE; - END IF; - IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'users') THEN - TRUNCATE TABLE users CASCADE; - END IF; - IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'calendars') THEN - TRUNCATE TABLE calendars CASCADE; - END IF; - IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'calendarinstances') THEN - TRUNCATE TABLE calendarinstances CASCADE; - END IF; - IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'calendarobjects') THEN - TRUNCATE TABLE calendarobjects CASCADE; - END IF; - END $$; - """) + if not _has_caldav_marker(request): + yield + return + + import psycopg # noqa: PLC0415 # pylint: disable=import-outside-toplevel + + db_settings = settings.DATABASES["default"] + conn = psycopg.connect( + host=db_settings["HOST"], + port=db_settings["PORT"], + dbname="calendars", # SabreDAV always uses this DB + user=db_settings["USER"], + password=db_settings["PASSWORD"], + ) + conn.autocommit = True + try: + with conn.cursor() as cur: # pylint: disable=no-member + for table in [ + "calendarobjects", + "calendarinstances", + "calendars", + "principals", + ]: + cur.execute(f"TRUNCATE TABLE {table} CASCADE") + finally: + conn.close() # pylint: disable=no-member yield +@pytest.fixture(autouse=True) +def disconnect_caldav_signals_for_unit_tests(request): + """Disconnect CalDAV signal handlers for non-CalDAV tests. + + Prevents non-CalDAV tests from hitting the real SabreDAV server + (e.g. via post_save signal when UserFactory creates a user), + which would interfere with CalDAV E2E tests running concurrently + on another xdist worker. + """ + if _has_caldav_marker(request): + yield + return + + from django.contrib.auth import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + get_user_model, + ) + from django.db.models.signals import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + post_save, + pre_delete, + ) + + from core.signals import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + delete_user_caldav_data, + provision_default_calendar, + ) + + user_model = get_user_model() + post_save.disconnect(provision_default_calendar, sender=user_model) + pre_delete.disconnect(delete_user_caldav_data, sender=user_model) + yield + post_save.connect(provision_default_calendar, sender=user_model) + pre_delete.connect(delete_user_caldav_data, sender=user_model) + + @pytest.fixture(autouse=True) def clear_cache(): """Fixture to clear the cache after each test.""" @@ -58,16 +100,7 @@ def clear_cache(): # Clear functools.cache for functions decorated with @functools.cache -@pytest.fixture -def mock_user_teams(): - """Mock for the "teams" property on the User model.""" - with mock.patch( - "core.models.User.teams", new_callable=mock.PropertyMock - ) as mock_teams: - yield mock_teams - - -def resource_server_backend_setup(settings): +def resource_server_backend_setup(settings): # pylint: disable=redefined-outer-name """ A fixture to create a user token for testing. """ @@ -91,7 +124,7 @@ def resource_server_backend_setup(settings): @pytest.fixture -def resource_server_backend_conf(settings): +def resource_server_backend_conf(settings): # pylint: disable=redefined-outer-name """ A fixture to create a user token for testing. """ @@ -100,7 +133,7 @@ def resource_server_backend_conf(settings): @pytest.fixture -def resource_server_backend(settings): +def resource_server_backend(settings): # pylint: disable=redefined-outer-name """ A fixture to create a user token for testing. Including a mocked introspection endpoint. diff --git a/src/backend/core/tests/test_api_users.py b/src/backend/core/tests/test_api_users.py index a837808..36a71c4 100644 --- a/src/backend/core/tests/test_api_users.py +++ b/src/backend/core/tests/test_api_users.py @@ -43,29 +43,32 @@ def test_api_users_list_authenticated(): "/api/v1.0/users/", ) assert response.status_code == 200 - assert response.json() == [] + assert response.json()["results"] == [] def test_api_users_list_query_inactive(): """Inactive users should not be listed.""" user = factories.UserFactory() + org = user.organization client = APIClient() client.force_login(user) - factories.UserFactory(email="john.doe@example.com", is_active=False) - lennon = factories.UserFactory(email="john.lennon@example.com") + factories.UserFactory( + email="john.doe@example.com", is_active=False, organization=org + ) + lennon = factories.UserFactory(email="john.lennon@example.com", organization=org) # Use email query to get exact match response = client.get("/api/v1.0/users/?q=john.lennon@example.com") assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [str(lennon.id)] # Inactive user should not be returned even with exact match response = client.get("/api/v1.0/users/?q=john.doe@example.com") assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [] @@ -83,16 +86,16 @@ def test_api_users_list_query_short_queries(): response = client.get("/api/v1.0/users/?q=jo") assert response.status_code == 200 - assert response.json() == [] + assert response.json()["results"] == [] response = client.get("/api/v1.0/users/?q=john") assert response.status_code == 200 - assert response.json() == [] + assert response.json()["results"] == [] # Non-email queries (without @) return empty response = client.get("/api/v1.0/users/?q=john.") assert response.status_code == 200 - assert response.json() == [] + assert response.json()["results"] == [] def test_api_users_list_limit(settings): @@ -101,6 +104,7 @@ def test_api_users_list_limit(settings): should be limited to 10. """ user = factories.UserFactory() + org = user.organization client = APIClient() client.force_login(user) @@ -108,14 +112,14 @@ def test_api_users_list_limit(settings): # Use a base name with a length equal 5 to test that the limit is applied base_name = "alice" for i in range(15): - factories.UserFactory(email=f"{base_name}.{i}@example.com") + factories.UserFactory(email=f"{base_name}.{i}@example.com", organization=org) # Non-email queries (without @) return empty response = client.get( "/api/v1.0/users/?q=alice", ) assert response.status_code == 200 - assert response.json() == [] + assert response.json()["results"] == [] # Email queries require exact match settings.API_USERS_LIST_LIMIT = 100 @@ -123,7 +127,7 @@ def test_api_users_list_limit(settings): "/api/v1.0/users/?q=alice.0@example.com", ) assert response.status_code == 200 - assert len(response.json()) == 1 + assert len(response.json()["results"]) == 1 def test_api_users_list_throttling_authenticated(settings): @@ -157,19 +161,20 @@ def test_api_users_list_query_email(settings): settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["user_list_burst"] = "9999/minute" user = factories.UserFactory() + org = user.organization client = APIClient() client.force_login(user) - dave = factories.UserFactory(email="david.bowman@work.com") - factories.UserFactory(email="nicole.bowman@work.com") + dave = factories.UserFactory(email="david.bowman@work.com", organization=org) + factories.UserFactory(email="nicole.bowman@work.com", organization=org) # Exact match works response = client.get( "/api/v1.0/users/?q=david.bowman@work.com", ) assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [str(dave.id)] # Case-insensitive match works @@ -177,7 +182,7 @@ def test_api_users_list_query_email(settings): "/api/v1.0/users/?q=David.Bowman@Work.COM", ) assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [str(dave.id)] # Typos don't match (exact match only) @@ -185,43 +190,48 @@ def test_api_users_list_query_email(settings): "/api/v1.0/users/?q=davig.bovman@worm.com", ) assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [] response = client.get( "/api/v1.0/users/?q=davig.bovman@worm.cop", ) assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [] def test_api_users_list_query_email_matching(): """Email queries return exact matches only (case-insensitive).""" user = factories.UserFactory() + org = user.organization client = APIClient() client.force_login(user) - user1 = factories.UserFactory(email="alice.johnson@example.gouv.fr") - factories.UserFactory(email="alice.johnnson@example.gouv.fr") - factories.UserFactory(email="alice.kohlson@example.gouv.fr") - user4 = factories.UserFactory(email="alicia.johnnson@example.gouv.fr") - factories.UserFactory(email="alicia.johnnson@example.gov.uk") - factories.UserFactory(email="alice.thomson@example.gouv.fr") + user1 = factories.UserFactory( + email="alice.johnson@example.gouv.fr", organization=org + ) + factories.UserFactory(email="alice.johnnson@example.gouv.fr", organization=org) + factories.UserFactory(email="alice.kohlson@example.gouv.fr", organization=org) + user4 = factories.UserFactory( + email="alicia.johnnson@example.gouv.fr", organization=org + ) + factories.UserFactory(email="alicia.johnnson@example.gov.uk", organization=org) + factories.UserFactory(email="alice.thomson@example.gouv.fr", organization=org) # Exact match returns only that user response = client.get( "/api/v1.0/users/?q=alice.johnson@example.gouv.fr", ) assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [str(user1.id)] # Different email returns different user response = client.get("/api/v1.0/users/?q=alicia.johnnson@example.gouv.fr") assert response.status_code == 200 - user_ids = [user["id"] for user in response.json()] + user_ids = [user["id"] for user in response.json()["results"]] assert user_ids == [str(user4.id)] @@ -260,9 +270,13 @@ def test_api_users_retrieve_me_authenticated(): "id": str(user.id), "email": user.email, "full_name": user.full_name, - "short_name": user.short_name, "language": user.language, "can_access": True, + "can_admin": True, + "organization": { + "id": str(user.organization.id), + "name": user.organization.name, + }, } diff --git a/src/backend/core/tests/test_caldav_proxy.py b/src/backend/core/tests/test_caldav_proxy.py index fb6a748..dba33c5 100644 --- a/src/backend/core/tests/test_caldav_proxy.py +++ b/src/backend/core/tests/test_caldav_proxy.py @@ -27,7 +27,7 @@ class TestCalDAVProxy: def test_proxy_requires_authentication(self): """Test that unauthenticated requests return 401.""" client = APIClient() - response = client.generic("PROPFIND", "/api/v1.0/caldav/") + response = client.generic("PROPFIND", "/caldav/") assert response.status_code == HTTP_401_UNAUTHORIZED @responses.activate @@ -49,7 +49,7 @@ class TestCalDAVProxy: ) ) - client.generic("PROPFIND", "/api/v1.0/caldav/") + client.generic("PROPFIND", "/caldav/") # Verify request was made to CalDAV server assert len(responses.calls) == 1 @@ -77,7 +77,7 @@ class TestCalDAVProxy: responses.add( responses.Response( method="PROPFIND", - url=f"{caldav_url}/api/v1.0/caldav/", + url=f"{caldav_url}/caldav/", status=HTTP_207_MULTI_STATUS, body='', headers={"Content-Type": "application/xml"}, @@ -88,7 +88,7 @@ class TestCalDAVProxy: malicious_email = "attacker@example.com" client.generic( "PROPFIND", - "/api/v1.0/caldav/", + "/caldav/", HTTP_X_FORWARDED_USER=malicious_email, ) @@ -107,10 +107,6 @@ class TestCalDAVProxy: "X-Forwarded-User should NOT use client-sent header value" ) - @pytest.mark.skipif( - not settings.CALDAV_URL, - reason="CalDAV server URL not configured - integration test requires real server", - ) def test_proxy_propfind_response_contains_prefixed_urls(self): """PROPFIND responses should contain URLs with proxy prefix. @@ -130,7 +126,7 @@ class TestCalDAVProxy: ) response = client.generic( "PROPFIND", - "/api/v1.0/caldav/", + "/caldav/", data=propfind_body, content_type="application/xml", ) @@ -154,8 +150,8 @@ class TestCalDAVProxy: if href and ( href.startswith("/principals/") or href.startswith("/calendars/") ): - assert href.startswith("/api/v1.0/caldav/"), ( - f"Expected URL to start with /api/v1.0/caldav/, " + assert href.startswith("/caldav/"), ( + f"Expected URL to start with /caldav/, " f"got {href}. BaseUriPlugin is not using " f"X-Forwarded-Prefix correctly. Full response: " f"{response.content.decode('utf-8', errors='ignore')}" @@ -178,7 +174,7 @@ class TestCalDAVProxy: propfind_xml = """ - /api/v1.0/caldav/calendars/test@example.com/calendar-id/ + /caldav/calendars/users/test@example.com/calendar-id/ @@ -193,14 +189,14 @@ class TestCalDAVProxy: responses.add( responses.Response( method="PROPFIND", - url=f"{caldav_url}/api/v1.0/caldav/", + url=f"{caldav_url}/caldav/", status=HTTP_207_MULTI_STATUS, body=propfind_xml, headers={"Content-Type": "application/xml"}, ) ) - response = client.generic("PROPFIND", "/api/v1.0/caldav/") + response = client.generic("PROPFIND", "/caldav/") assert response.status_code == HTTP_207_MULTI_STATUS @@ -213,7 +209,7 @@ class TestCalDAVProxy: # Verify the URL is passed through unchanged (sabre/dav should generate it with prefix) href = href_elem.text - assert href == "/api/v1.0/caldav/calendars/test@example.com/calendar-id/", ( + assert href == "/caldav/calendars/users/test@example.com/calendar-id/", ( f"Expected URL to be passed through unchanged, got {href}" ) @@ -234,7 +230,7 @@ class TestCalDAVProxy: propfind_xml = """ - /api/v1.0/caldav/principals/test@example.com/ + /caldav/principals/users/test@example.com/ @@ -248,14 +244,14 @@ class TestCalDAVProxy: responses.add( responses.Response( method="PROPFIND", - url=f"{caldav_url}/api/v1.0/caldav/", + url=f"{caldav_url}/caldav/", status=HTTP_207_MULTI_STATUS, body=propfind_xml, headers={"Content-Type": "application/xml"}, ) ) - response = client.generic("PROPFIND", "/api/v1.0/caldav/") + response = client.generic("PROPFIND", "/caldav/") assert response.status_code == HTTP_207_MULTI_STATUS @@ -268,7 +264,7 @@ class TestCalDAVProxy: # Verify the URL is passed through unchanged (sabre/dav should generate it with prefix) href = href_elem.text - assert href == "/api/v1.0/caldav/principals/test@example.com/", ( + assert href == "/caldav/principals/users/test@example.com/", ( f"Expected URL to be passed through unchanged, got {href}" ) @@ -283,7 +279,7 @@ class TestCalDAVProxy: responses.add( responses.Response( method="PROPFIND", - url=f"{caldav_url}/api/v1.0/caldav/principals/test@example.com/", + url=f"{caldav_url}/caldav/principals/users/test@example.com/", status=HTTP_207_MULTI_STATUS, body='', headers={"Content-Type": "application/xml"}, @@ -291,14 +287,12 @@ class TestCalDAVProxy: ) # Request a specific path - client.generic("PROPFIND", "/api/v1.0/caldav/principals/test@example.com/") + client.generic("PROPFIND", "/caldav/principals/users/test@example.com/") # Verify the request was made to the correct URL assert len(responses.calls) == 1 request = responses.calls[0].request - assert ( - request.url == f"{caldav_url}/api/v1.0/caldav/principals/test@example.com/" - ) + assert request.url == f"{caldav_url}/caldav/principals/users/test@example.com/" @responses.activate def test_proxy_handles_options_request(self): @@ -307,7 +301,7 @@ class TestCalDAVProxy: client = APIClient() client.force_login(user) - response = client.options("/api/v1.0/caldav/") + response = client.options("/caldav/") assert response.status_code == HTTP_200_OK assert "Access-Control-Allow-Methods" in response @@ -319,9 +313,7 @@ class TestCalDAVProxy: client = APIClient() client.force_login(user) - response = client.generic( - "PROPFIND", "/api/v1.0/caldav/calendars/../../etc/passwd" - ) + response = client.generic("PROPFIND", "/caldav/calendars/../../etc/passwd") assert response.status_code == HTTP_400_BAD_REQUEST def test_proxy_rejects_non_caldav_path(self): @@ -330,7 +322,16 @@ class TestCalDAVProxy: client = APIClient() client.force_login(user) - response = client.generic("PROPFIND", "/api/v1.0/caldav/etc/passwd") + response = client.generic("PROPFIND", "/caldav/etc/passwd") + assert response.status_code == HTTP_400_BAD_REQUEST + + def test_proxy_rejects_internal_api_path(self): + """Test that proxy explicitly blocks /internal-api/ paths.""" + user = factories.UserFactory(email="test@example.com") + client = APIClient() + client.force_login(user) + + response = client.generic("POST", "/caldav/internal-api/resources/") assert response.status_code == HTTP_400_BAD_REQUEST @@ -343,11 +344,11 @@ class TestValidateCaldavProxyPath: def test_calendars_path_is_valid(self): """Standard calendars path should be valid.""" - assert validate_caldav_proxy_path("calendars/user@ex.com/uuid/") is True + assert validate_caldav_proxy_path("calendars/users/user@ex.com/uuid/") is True def test_principals_path_is_valid(self): """Standard principals path should be valid.""" - assert validate_caldav_proxy_path("principals/user@ex.com/") is True + assert validate_caldav_proxy_path("principals/users/user@ex.com/") is True def test_traversal_is_rejected(self): """Directory traversal attempts should be rejected.""" @@ -363,4 +364,24 @@ class TestValidateCaldavProxyPath: def test_leading_slash_calendars_is_valid(self): """Paths with leading slash should still be valid.""" - assert validate_caldav_proxy_path("/calendars/user@ex.com/uuid/") is True + assert validate_caldav_proxy_path("/calendars/users/user@ex.com/uuid/") is True + + def test_internal_api_is_rejected(self): + """Internal API paths should be explicitly blocked.""" + assert validate_caldav_proxy_path("internal-api/resources/") is False + + def test_internal_api_with_leading_slash_is_rejected(self): + """Internal API paths with leading slash should be blocked.""" + assert validate_caldav_proxy_path("/internal-api/import/user/cal") is False + + def test_encoded_traversal_is_rejected(self): + """URL-encoded directory traversal should be rejected.""" + assert validate_caldav_proxy_path("calendars/%2e%2e/%2e%2e/etc/passwd") is False + + def test_encoded_internal_api_is_rejected(self): + """URL-encoded internal-api path should be blocked.""" + assert validate_caldav_proxy_path("%69nternal-api/resources/") is False + + def test_encoded_null_byte_is_rejected(self): + """URL-encoded null byte should be rejected.""" + assert validate_caldav_proxy_path("calendars/user%00/") is False diff --git a/src/backend/core/tests/test_caldav_scheduling.py b/src/backend/core/tests/test_caldav_scheduling.py index 8767981..7567344 100644 --- a/src/backend/core/tests/test_caldav_scheduling.py +++ b/src/backend/core/tests/test_caldav_scheduling.py @@ -72,13 +72,10 @@ def create_test_server() -> tuple: @pytest.mark.django_db +@pytest.mark.xdist_group("caldav") class TestCalDAVScheduling: """Tests for CalDAV scheduling callback when creating events with attendees.""" - @pytest.mark.skipif( - not settings.CALDAV_URL, - reason="CalDAV server URL not configured - integration test requires real server", - ) def test_scheduling_callback_received_when_creating_event_with_attendee( # noqa: PLR0915 # pylint: disable=too-many-locals,too-many-statements self, ): @@ -125,8 +122,8 @@ class TestCalDAVScheduling: try: # Create an event with an attendee - client = service.caldav._get_client(organizer) # pylint: disable=protected-access - calendar_url = f"{settings.CALDAV_URL}{caldav_path}" + client = service._get_client(organizer) # pylint: disable=protected-access + calendar_url = service._calendar_url(caldav_path) # pylint: disable=protected-access # Add custom callback URL header to the client # The CalDAV server will use this URL for the callback diff --git a/src/backend/core/tests/test_caldav_service.py b/src/backend/core/tests/test_caldav_service.py index b83f20c..c727dc7 100644 --- a/src/backend/core/tests/test_caldav_service.py +++ b/src/backend/core/tests/test_caldav_service.py @@ -1,7 +1,5 @@ """Tests for CalDAV service integration.""" -from django.conf import settings - import pytest from core import factories @@ -9,6 +7,7 @@ from core.services.caldav_service import CalDAVClient, CalendarService @pytest.mark.django_db +@pytest.mark.xdist_group("caldav") class TestCalDAVClient: """Tests for CalDAVClient authentication and communication.""" @@ -30,10 +29,6 @@ class TestCalDAVClient: assert "X-Forwarded-User" in dav_client.headers assert dav_client.headers["X-Forwarded-User"] == user.email - @pytest.mark.skipif( - not settings.CALDAV_URL, - reason="CalDAV server URL not configured", - ) def test_create_calendar_authenticates_with_caldav_server(self): """Test that calendar creation authenticates successfully with CalDAV server.""" user = factories.UserFactory(email="test@example.com") @@ -65,10 +60,6 @@ class TestCalDAVClient: assert isinstance(caldav_path, str) assert "calendars/" in caldav_path - @pytest.mark.skipif( - not settings.CALDAV_URL, - reason="CalDAV server URL not configured", - ) def test_create_calendar_with_color_persists(self): """Test that creating a calendar with a color saves it in CalDAV.""" user = factories.UserFactory(email="color-test@example.com") @@ -79,7 +70,7 @@ class TestCalDAVClient: caldav_path = service.create_calendar(user, name="Red Calendar", color=color) # Fetch the calendar info and verify the color was persisted - info = service.caldav.get_calendar_info(user, caldav_path) + info = service.get_calendar_info(user, caldav_path) assert info is not None assert info["color"] == color assert info["name"] == "Red Calendar" diff --git a/src/backend/core/tests/test_calendar_subscription_api.py b/src/backend/core/tests/test_calendar_subscription_api.py index 1a47d96..8c82a71 100644 --- a/src/backend/core/tests/test_calendar_subscription_api.py +++ b/src/backend/core/tests/test_calendar_subscription_api.py @@ -1,40 +1,37 @@ -"""Tests for calendar subscription token API.""" - -from urllib.parse import quote - -from django.urls import reverse +"""Tests for iCal feed channel creation via the channels API.""" import pytest from rest_framework.status import ( HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, - HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, - HTTP_404_NOT_FOUND, ) from rest_framework.test import APIClient from core import factories -from core.models import CalendarSubscriptionToken +from core.models import Channel + +CHANNELS_URL = "/api/v1.0/channels/" @pytest.mark.django_db -class TestSubscriptionTokenViewSet: - """Tests for the new standalone SubscriptionTokenViewSet.""" +class TestICalFeedChannels: + """Tests for ical-feed channel creation via ChannelViewSet.""" - def test_create_subscription_token(self): - """Test creating a subscription token for a calendar.""" + def test_create_ical_feed_channel(self): + """Test creating an ical-feed channel for a calendar.""" user = factories.UserFactory() - caldav_path = f"/calendars/{user.email}/test-calendar-uuid/" + caldav_path = f"/calendars/users/{user.email}/test-calendar-uuid/" client = APIClient() client.force_login(user) - url = reverse("subscription-tokens-list") response = client.post( - url, + CHANNELS_URL, { + "name": "My Test Calendar", + "type": "ical-feed", "caldav_path": caldav_path, "calendar_name": "My Test Calendar", }, @@ -47,233 +44,210 @@ class TestSubscriptionTokenViewSet: assert "/ical/" in response.data["url"] assert ".ics" in response.data["url"] assert response.data["caldav_path"] == caldav_path - assert response.data["calendar_name"] == "My Test Calendar" + assert response.data["type"] == "ical-feed" - # Verify token was created in database - assert CalendarSubscriptionToken.objects.filter( - owner=user, caldav_path=caldav_path + # Verify channel was created in database + assert Channel.objects.filter( + user=user, caldav_path=caldav_path, type="ical-feed" ).exists() - def test_create_subscription_token_normalizes_path(self): + def test_create_ical_feed_normalizes_path(self): """Test that caldav_path is normalized to have leading/trailing slashes.""" user = factories.UserFactory() - caldav_path = f"calendars/{user.email}/test-uuid" # No leading/trailing slash + caldav_path = f"calendars/users/{user.email}/test-uuid" client = APIClient() client.force_login(user) - url = reverse("subscription-tokens-list") response = client.post( - url, - {"caldav_path": caldav_path}, + CHANNELS_URL, + {"name": "Cal", "type": "ical-feed", "caldav_path": caldav_path}, format="json", ) assert response.status_code == HTTP_201_CREATED - # Path should be normalized - assert response.data["caldav_path"] == f"/calendars/{user.email}/test-uuid/" + assert ( + response.data["caldav_path"] == f"/calendars/users/{user.email}/test-uuid/" + ) - def test_create_subscription_token_returns_existing(self): - """Test that creating a token when one exists returns the existing one.""" - subscription = factories.CalendarSubscriptionTokenFactory() + def test_create_ical_feed_returns_existing(self): + """Test that creating an ical-feed channel when one exists returns it.""" + channel = factories.ICalFeedChannelFactory() client = APIClient() - client.force_login(subscription.owner) + client.force_login(channel.user) - url = reverse("subscription-tokens-list") response = client.post( - url, + CHANNELS_URL, { - "caldav_path": subscription.caldav_path, + "name": "Updated Name", + "type": "ical-feed", + "caldav_path": channel.caldav_path, "calendar_name": "Updated Name", }, format="json", ) assert response.status_code == HTTP_200_OK - assert response.data["token"] == str(subscription.token) # Name should be updated - subscription.refresh_from_db() - assert subscription.calendar_name == "Updated Name" + channel.refresh_from_db() + assert channel.settings["calendar_name"] == "Updated Name" - def test_get_subscription_token_by_path(self): - """Test retrieving an existing subscription token by CalDAV path.""" - subscription = factories.CalendarSubscriptionTokenFactory() + def test_list_ical_feed_channels(self): + """Test filtering channels by type=ical-feed.""" + user = factories.UserFactory() client = APIClient() - client.force_login(subscription.owner) + client.force_login(user) - url = reverse("subscription-tokens-by-path") - response = client.get(url, {"caldav_path": subscription.caldav_path}) + # Create one ical-feed and one caldav channel + client.post( + CHANNELS_URL, + { + "name": "Feed", + "type": "ical-feed", + "caldav_path": f"/calendars/users/{user.email}/cal1/", + }, + format="json", + ) + client.post( + CHANNELS_URL, + {"name": "CalDAV Channel"}, + format="json", + ) + # Filter by type + response = client.get(CHANNELS_URL, {"type": "ical-feed"}) assert response.status_code == HTTP_200_OK - assert response.data["token"] == str(subscription.token) - assert "url" in response.data + assert len(response.data) == 1 + assert response.data[0]["type"] == "ical-feed" - def test_get_subscription_token_not_found(self): - """Test retrieving token when none exists.""" - user = factories.UserFactory() - caldav_path = f"/calendars/{user.email}/nonexistent/" + # Without filter, both show up + response = client.get(CHANNELS_URL) + assert len(response.data) == 2 + + def test_delete_ical_feed_channel(self): + """Test deleting an ical-feed channel.""" + channel = factories.ICalFeedChannelFactory() client = APIClient() - client.force_login(user) + client.force_login(channel.user) - url = reverse("subscription-tokens-by-path") - response = client.get(url, {"caldav_path": caldav_path}) - - assert response.status_code == HTTP_404_NOT_FOUND - - def test_get_subscription_token_missing_path(self): - """Test that missing caldav_path query param returns 400.""" - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - url = reverse("subscription-tokens-by-path") - response = client.get(url) - - assert response.status_code == HTTP_400_BAD_REQUEST - - def test_delete_subscription_token(self): - """Test revoking a subscription token.""" - subscription = factories.CalendarSubscriptionTokenFactory() - client = APIClient() - client.force_login(subscription.owner) - - base_url = reverse("subscription-tokens-by-path") - url = f"{base_url}?caldav_path={quote(subscription.caldav_path, safe='')}" - response = client.delete(url) + response = client.delete(f"{CHANNELS_URL}{channel.pk}/") assert response.status_code == HTTP_204_NO_CONTENT - assert not CalendarSubscriptionToken.objects.filter(pk=subscription.pk).exists() + assert not Channel.objects.filter(pk=channel.pk).exists() - def test_delete_subscription_token_not_found(self): - """Test deleting token when none exists.""" - user = factories.UserFactory() - caldav_path = f"/calendars/{user.email}/nonexistent/" - client = APIClient() - client.force_login(user) - - base_url = reverse("subscription-tokens-by-path") - url = f"{base_url}?caldav_path={quote(caldav_path, safe='')}" - response = client.delete(url) - - assert response.status_code == HTTP_404_NOT_FOUND - - def test_non_owner_cannot_create_token(self): - """Test that users cannot create tokens for other users' calendars.""" + def test_non_owner_cannot_create_ical_feed(self): + """Test that users cannot create ical-feed channels for others' calendars.""" user = factories.UserFactory() other_user = factories.UserFactory() - caldav_path = f"/calendars/{other_user.email}/test-calendar/" + caldav_path = f"/calendars/users/{other_user.email}/test-calendar/" client = APIClient() client.force_login(user) - url = reverse("subscription-tokens-list") response = client.post( - url, - {"caldav_path": caldav_path}, + CHANNELS_URL, + { + "name": "Stolen", + "type": "ical-feed", + "caldav_path": caldav_path, + }, format="json", ) assert response.status_code == HTTP_403_FORBIDDEN - def test_non_owner_cannot_get_token(self): - """Test that users cannot get tokens for other users' calendars.""" - subscription = factories.CalendarSubscriptionTokenFactory() + def test_non_owner_cannot_list_others_channels(self): + """Test that users only see their own channels.""" + factories.ICalFeedChannelFactory() other_user = factories.UserFactory() client = APIClient() client.force_login(other_user) - url = reverse("subscription-tokens-by-path") - response = client.get(url, {"caldav_path": subscription.caldav_path}) + response = client.get(CHANNELS_URL, {"type": "ical-feed"}) + assert response.status_code == HTTP_200_OK + assert len(response.data) == 0 - assert response.status_code == HTTP_403_FORBIDDEN - - def test_non_owner_cannot_delete_token(self): - """Test that users cannot delete tokens for other users' calendars.""" - subscription = factories.CalendarSubscriptionTokenFactory() - other_user = factories.UserFactory() - client = APIClient() - client.force_login(other_user) - - base_url = reverse("subscription-tokens-by-path") - url = f"{base_url}?caldav_path={quote(subscription.caldav_path, safe='')}" - response = client.delete(url) - - assert response.status_code == HTTP_403_FORBIDDEN - # Token should still exist - assert CalendarSubscriptionToken.objects.filter(pk=subscription.pk).exists() - - def test_unauthenticated_cannot_create_token(self): - """Test that unauthenticated users cannot create tokens.""" + def test_unauthenticated_cannot_create(self): + """Test that unauthenticated users cannot create channels.""" user = factories.UserFactory() - caldav_path = f"/calendars/{user.email}/test-calendar/" + caldav_path = f"/calendars/users/{user.email}/test-calendar/" client = APIClient() - url = reverse("subscription-tokens-list") response = client.post( - url, - {"caldav_path": caldav_path}, + CHANNELS_URL, + { + "name": "Feed", + "type": "ical-feed", + "caldav_path": caldav_path, + }, format="json", ) assert response.status_code == HTTP_401_UNAUTHORIZED - def test_unauthenticated_cannot_get_token(self): - """Test that unauthenticated users cannot get tokens.""" - subscription = factories.CalendarSubscriptionTokenFactory() - client = APIClient() - - url = reverse("subscription-tokens-by-path") - response = client.get(url, {"caldav_path": subscription.caldav_path}) - - assert response.status_code == HTTP_401_UNAUTHORIZED - def test_regenerate_token(self): """Test regenerating a token by delete + create.""" - subscription = factories.CalendarSubscriptionTokenFactory() - old_token = subscription.token + channel = factories.ICalFeedChannelFactory() + old_token = channel.encrypted_settings["token"] client = APIClient() - client.force_login(subscription.owner) + client.force_login(channel.user) - base_by_path_url = reverse("subscription-tokens-by-path") - by_path_url = ( - f"{base_by_path_url}?caldav_path={quote(subscription.caldav_path, safe='')}" - ) - create_url = reverse("subscription-tokens-list") - - # Delete old token - response = client.delete(by_path_url) + # Delete old channel + response = client.delete(f"{CHANNELS_URL}{channel.pk}/") assert response.status_code == HTTP_204_NO_CONTENT - # Create new token + # Create new one for the same path response = client.post( - create_url, - {"caldav_path": subscription.caldav_path}, + CHANNELS_URL, + { + "name": "Feed", + "type": "ical-feed", + "caldav_path": channel.caldav_path, + }, format="json", ) assert response.status_code == HTTP_201_CREATED - assert response.data["token"] != str(old_token) + assert response.data["token"] != old_token def test_unique_constraint_per_owner_calendar(self): - """Test that only one token can exist per owner+caldav_path.""" - subscription = factories.CalendarSubscriptionTokenFactory() - - # Try to create another token for the same path - should return existing + """Test that only one ical-feed channel exists per owner+caldav_path.""" + channel = factories.ICalFeedChannelFactory() client = APIClient() - client.force_login(subscription.owner) + client.force_login(channel.user) - url = reverse("subscription-tokens-list") + # Try to create another - should return existing response = client.post( - url, - {"caldav_path": subscription.caldav_path}, + CHANNELS_URL, + { + "name": "Duplicate", + "type": "ical-feed", + "caldav_path": channel.caldav_path, + }, format="json", ) - # Should return the existing token, not create a new one assert response.status_code == HTTP_200_OK - assert response.data["token"] == str(subscription.token) - assert ( - CalendarSubscriptionToken.objects.filter(owner=subscription.owner).count() - == 1 + assert Channel.objects.filter(user=channel.user, type="ical-feed").count() == 1 + + def test_url_contains_slugified_calendar_name(self): + """Test that the URL contains the slugified calendar name.""" + user = factories.UserFactory() + caldav_path = f"/calendars/users/{user.email}/cal/" + client = APIClient() + client.force_login(user) + + response = client.post( + CHANNELS_URL, + { + "name": "My Awesome Calendar", + "type": "ical-feed", + "caldav_path": caldav_path, + "calendar_name": "My Awesome Calendar", + }, + format="json", ) + assert response.status_code == HTTP_201_CREATED + assert "my-awesome-calendar.ics" in response.data["url"] + @pytest.mark.django_db class TestPathInjectionProtection: @@ -290,45 +264,40 @@ class TestPathInjectionProtection: @pytest.mark.parametrize( "malicious_suffix", [ - # Path traversal attacks "../other-calendar/", "../../etc/passwd/", - "..%2F..%2Fetc%2Fpasswd/", # URL-encoded traversal - # Query parameter injection + "..%2F..%2Fetc%2Fpasswd/", "uuid?export=true/", "uuid?admin=true/", - # Fragment injection "uuid#malicious/", - # Special characters that shouldn't be in calendar IDs "uuid;rm -rf/", "uuid|cat /etc/passwd/", "uuid$(whoami)/", "uuid`whoami`/", - # Double slashes "uuid//", "/uuid/", - # Spaces and other whitespace "uuid with spaces/", "uuid\ttab/", - # Unicode tricks - "uuid\u002e\u002e/", # Unicode dots + "uuid\u002e\u002e/", ], ) - def test_create_token_rejects_malicious_calendar_id(self, malicious_suffix): + def test_create_rejects_malicious_calendar_id(self, malicious_suffix): """Test that malicious calendar IDs in paths are rejected.""" user = factories.UserFactory() - caldav_path = f"/calendars/{user.email}/{malicious_suffix}" + caldav_path = f"/calendars/users/{user.email}/{malicious_suffix}" client = APIClient() client.force_login(user) - url = reverse("subscription-tokens-list") response = client.post( - url, - {"caldav_path": caldav_path}, + CHANNELS_URL, + { + "name": "Bad", + "type": "ical-feed", + "caldav_path": caldav_path, + }, format="json", ) - # Should be rejected - either 403 (invalid format) or path doesn't normalize assert response.status_code == HTTP_403_FORBIDDEN, ( f"Path '{caldav_path}' should be rejected but got {response.status_code}" ) @@ -336,31 +305,30 @@ class TestPathInjectionProtection: @pytest.mark.parametrize( "malicious_path", [ - # Completely wrong structure "/etc/passwd/", "/admin/calendars/user@test.com/uuid/", "/../calendars/user@test.com/uuid/", - # Missing segments "/calendars/", "/calendars/user@test.com/", - # Path traversal to access another user's calendar "/calendars/victim@test.com/../attacker@test.com/uuid/", ], ) - def test_create_token_rejects_malformed_paths(self, malicious_path): + def test_create_rejects_malformed_paths(self, malicious_path): """Test that malformed CalDAV paths are rejected.""" user = factories.UserFactory(email="attacker@test.com") client = APIClient() client.force_login(user) - url = reverse("subscription-tokens-list") response = client.post( - url, - {"caldav_path": malicious_path}, + CHANNELS_URL, + { + "name": "Bad", + "type": "ical-feed", + "caldav_path": malicious_path, + }, format="json", ) - # Should be rejected assert response.status_code == HTTP_403_FORBIDDEN, ( f"Path '{malicious_path}' should be rejected but got {response.status_code}" ) @@ -368,21 +336,23 @@ class TestPathInjectionProtection: def test_path_traversal_to_other_user_calendar_rejected(self): """Test that path traversal to access another user's calendar is blocked.""" attacker = factories.UserFactory(email="attacker@example.com") - victim = factories.UserFactory(email="victim@example.com") + factories.UserFactory(email="victim@example.com") client = APIClient() client.force_login(attacker) - # Try to access victim's calendar via path traversal malicious_paths = [ - f"/calendars/{attacker.email}/../{victim.email}/secret-calendar/", - f"/calendars/{victim.email}/secret-calendar/", # Direct access + f"/calendars/{attacker.email}/../victim@example.com/secret-calendar/", + "/calendars/victim@example.com/secret-calendar/", ] - url = reverse("subscription-tokens-list") for path in malicious_paths: response = client.post( - url, - {"caldav_path": path}, + CHANNELS_URL, + { + "name": "Bad", + "type": "ical-feed", + "caldav_path": path, + }, format="json", ) assert response.status_code == HTTP_403_FORBIDDEN, ( @@ -392,15 +362,19 @@ class TestPathInjectionProtection: def test_valid_uuid_path_accepted(self): """Test that valid UUID-style calendar IDs are accepted.""" user = factories.UserFactory() - # Standard UUID format - caldav_path = f"/calendars/{user.email}/550e8400-e29b-41d4-a716-446655440000/" + caldav_path = ( + f"/calendars/users/{user.email}/550e8400-e29b-41d4-a716-446655440000/" + ) client = APIClient() client.force_login(user) - url = reverse("subscription-tokens-list") response = client.post( - url, - {"caldav_path": caldav_path}, + CHANNELS_URL, + { + "name": "Good", + "type": "ical-feed", + "caldav_path": caldav_path, + }, format="json", ) @@ -409,43 +383,18 @@ class TestPathInjectionProtection: def test_valid_alphanumeric_path_accepted(self): """Test that valid alphanumeric calendar IDs are accepted.""" user = factories.UserFactory() - # Alphanumeric with hyphens (allowed by regex) - caldav_path = f"/calendars/{user.email}/my-calendar-2024/" + caldav_path = f"/calendars/users/{user.email}/my-calendar-2024/" client = APIClient() client.force_login(user) - url = reverse("subscription-tokens-list") response = client.post( - url, - {"caldav_path": caldav_path}, + CHANNELS_URL, + { + "name": "Good", + "type": "ical-feed", + "caldav_path": caldav_path, + }, format="json", ) assert response.status_code == HTTP_201_CREATED - - def test_get_token_with_malicious_path_rejected(self): - """Test that GET requests with malicious paths are rejected.""" - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - malicious_path = f"/calendars/{user.email}/../../../etc/passwd/" - - url = reverse("subscription-tokens-by-path") - response = client.get(url, {"caldav_path": malicious_path}) - - assert response.status_code == HTTP_403_FORBIDDEN - - def test_delete_token_with_malicious_path_rejected(self): - """Test that DELETE requests with malicious paths are rejected.""" - user = factories.UserFactory() - client = APIClient() - client.force_login(user) - - malicious_path = f"/calendars/{user.email}/../../../etc/passwd/" - - base_url = reverse("subscription-tokens-by-path") - url = f"{base_url}?caldav_path={quote(malicious_path, safe='')}" - response = client.delete(url) - - assert response.status_code == HTTP_403_FORBIDDEN diff --git a/src/backend/core/tests/test_channels.py b/src/backend/core/tests/test_channels.py new file mode 100644 index 0000000..ec7b98d --- /dev/null +++ b/src/backend/core/tests/test_channels.py @@ -0,0 +1,456 @@ +"""Tests for the Channel model and API.""" + +# pylint: disable=redefined-outer-name,missing-function-docstring,no-member + +import uuid +from unittest.mock import patch + +from django.core.exceptions import ValidationError + +import pytest +from rest_framework.test import APIClient + +from core import factories, models + +pytestmark = pytest.mark.django_db + +CHANNELS_URL = "/api/v1.0/channels/" + + +@pytest.fixture +def authenticated_client(): + """Return an (APIClient, User) pair with forced authentication.""" + user = factories.UserFactory() + client = APIClient() + client.force_authenticate(user=user) + return client, user + + +# --------------------------------------------------------------------------- +# Model tests +# --------------------------------------------------------------------------- + + +class TestChannelModel: + """Tests for the Channel model.""" + + def test_verify_token(self): + channel = factories.ChannelFactory() + token = channel.encrypted_settings["token"] + + assert channel.verify_token(token) + assert not channel.verify_token("wrong-token") + + def test_scope_validation_requires_at_least_one(self): + """Channel with no scope should fail validation.""" + channel = models.Channel(name="no-scope") + with pytest.raises(ValidationError): + channel.full_clean() + + def test_role_property(self): + """Role is stored in settings and accessible via property.""" + user = factories.UserFactory() + channel = models.Channel( + name="test", + user=user, + settings={"role": "editor"}, + ) + assert channel.role == "editor" + + channel.role = "admin" + assert channel.settings["role"] == "admin" + + def test_role_default(self): + """Role defaults to reader when not set.""" + user = factories.UserFactory() + channel = models.Channel(name="test", user=user) + assert channel.role == "reader" + + +# --------------------------------------------------------------------------- +# API tests +# --------------------------------------------------------------------------- + + +class TestChannelAPI: + """Tests for the Channel CRUD API.""" + + def test_create_channel(self, authenticated_client): + client, user = authenticated_client + response = client.post( + CHANNELS_URL, + {"name": "My Channel"}, + format="json", + ) + assert response.status_code == 201 + data = response.json() + assert data["name"] == "My Channel" + assert "token" in data # token revealed on creation + assert len(data["token"]) >= 20 + assert data["role"] == "reader" + assert data["user"] == str(user.pk) + + def test_create_channel_with_caldav_path(self, authenticated_client): + client, user = authenticated_client + caldav_path = f"/calendars/users/{user.email}/my-cal/" + response = client.post( + CHANNELS_URL, + {"name": "Cal Channel", "caldav_path": caldav_path}, + format="json", + ) + assert response.status_code == 201 + assert response.json()["caldav_path"] == caldav_path + + def test_create_channel_wrong_caldav_path(self, authenticated_client): + client, _user = authenticated_client + response = client.post( + CHANNELS_URL, + { + "name": "Bad", + "caldav_path": "/calendars/users/other@example.com/cal/", + }, + format="json", + ) + assert response.status_code == 403 + + def test_list_channels(self, authenticated_client): + client, _user = authenticated_client + # Create 2 channels + for i in range(2): + client.post( + CHANNELS_URL, + {"name": f"Channel {i}"}, + format="json", + ) + + response = client.get(CHANNELS_URL) + assert response.status_code == 200 + assert len(response.json()) == 2 + + def test_list_channels_only_own(self, authenticated_client): + """Users should only see their own channels.""" + client, _user = authenticated_client + # Create a channel for another user + factories.ChannelFactory() + + response = client.get(CHANNELS_URL) + assert response.status_code == 200 + assert len(response.json()) == 0 + + def test_retrieve_channel(self, authenticated_client): + client, _user = authenticated_client + create_resp = client.post( + CHANNELS_URL, + {"name": "Retrieve Me"}, + format="json", + ) + channel_id = create_resp.json()["id"] + + response = client.get(f"{CHANNELS_URL}{channel_id}/") + assert response.status_code == 200 + assert response.json()["name"] == "Retrieve Me" + assert "token" not in response.json() # token NOT in retrieve + + def test_delete_channel(self, authenticated_client): + client, _user = authenticated_client + create_resp = client.post( + CHANNELS_URL, + {"name": "Delete Me"}, + format="json", + ) + channel_id = create_resp.json()["id"] + + response = client.delete(f"{CHANNELS_URL}{channel_id}/") + assert response.status_code == 204 + assert not models.Channel.objects.filter(pk=channel_id).exists() + + def test_regenerate_token(self, authenticated_client): + client, _user = authenticated_client + create_resp = client.post( + CHANNELS_URL, + {"name": "Regen"}, + format="json", + ) + old_token = create_resp.json()["token"] + channel_id = create_resp.json()["id"] + + response = client.post(f"{CHANNELS_URL}{channel_id}/regenerate-token/") + assert response.status_code == 200 + new_token = response.json()["token"] + assert new_token != old_token + assert len(new_token) >= 20 + + def test_unauthenticated(self): + client = APIClient() + response = client.get(CHANNELS_URL) + assert response.status_code in (401, 403) + + +# --------------------------------------------------------------------------- +# CalDAV proxy channel auth tests +# --------------------------------------------------------------------------- + + +class TestCalDAVProxyChannelAuth: + """Tests for channel token authentication in the CalDAV proxy.""" + + @patch("core.api.viewsets_caldav.CalDAVHTTPClient") + def test_channel_token_auth_propfind(self, mock_http_cls): + """A reader channel token should allow PROPFIND.""" + user = factories.UserFactory() + channel = factories.ChannelFactory( + user=user, + settings={"role": "reader"}, + ) + token = channel.encrypted_settings["token"] + + mock_response = type( + "R", + (), + { + "status_code": 207, + "content": b"", + "headers": {"Content-Type": "application/xml"}, + }, + )() + mock_http_cls.build_base_headers.return_value = { + "X-Api-Key": "test", + "X-Forwarded-User": user.email, + } + + client = APIClient() + with patch( + "core.api.viewsets_caldav.requests.request", return_value=mock_response + ): + response = client.generic( + "PROPFIND", + f"/caldav/calendars/users/{user.email}/", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + HTTP_DEPTH="1", + ) + assert response.status_code == 207 + + @patch("core.api.viewsets_caldav.CalDAVHTTPClient") + def test_channel_token_reader_cannot_put(self, _mock_http_cls): + """A reader channel should NOT allow PUT.""" + user = factories.UserFactory() + channel = factories.ChannelFactory( + user=user, + settings={"role": "reader"}, + ) + token = channel.encrypted_settings["token"] + + client = APIClient() + response = client.put( + f"/caldav/calendars/users/{user.email}/cal/event.ics", + data=b"BEGIN:VCALENDAR", + content_type="text/calendar", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + ) + assert response.status_code == 403 + + @patch("core.api.viewsets_caldav.CalDAVHTTPClient") + def test_channel_token_editor_can_put(self, mock_http_cls): + """An editor channel should allow PUT.""" + user = factories.UserFactory() + channel = factories.ChannelFactory( + user=user, + settings={"role": "editor"}, + ) + token = channel.encrypted_settings["token"] + + mock_response = type( + "R", + (), + { + "status_code": 201, + "content": b"", + "headers": {"Content-Type": "text/plain"}, + }, + )() + mock_http_cls.build_base_headers.return_value = { + "X-Api-Key": "test", + "X-Forwarded-User": user.email, + } + + client = APIClient() + with patch( + "core.api.viewsets_caldav.requests.request", return_value=mock_response + ): + response = client.put( + f"/caldav/calendars/users/{user.email}/cal/event.ics", + data=b"BEGIN:VCALENDAR", + content_type="text/calendar", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + ) + assert response.status_code == 201 + + def test_channel_token_wrong_path(self): + """Channel should not access paths outside its user scope.""" + user = factories.UserFactory() + channel = factories.ChannelFactory( + user=user, + settings={"role": "reader"}, + ) + token = channel.encrypted_settings["token"] + + client = APIClient() + response = client.generic( + "PROPFIND", + "/caldav/calendars/users/other@example.com/cal/", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + ) + assert response.status_code == 403 + + def test_invalid_token(self): + """Invalid token should return 401.""" + user = factories.UserFactory() + channel = factories.ChannelFactory( + user=user, + settings={"role": "reader"}, + ) + + client = APIClient() + response = client.generic( + "PROPFIND", + "/caldav/calendars/", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN="invalid-token-12345", + ) + assert response.status_code == 401 + + def test_missing_channel_id(self): + """Token without channel ID should return 401.""" + client = APIClient() + response = client.generic( + "PROPFIND", + "/caldav/calendars/", + HTTP_X_CHANNEL_TOKEN="some-token", + ) + assert response.status_code == 401 + + def test_nonexistent_channel_id(self): + """Non-existent channel ID should return 401.""" + client = APIClient() + response = client.generic( + "PROPFIND", + "/caldav/calendars/", + HTTP_X_CHANNEL_ID=str(uuid.uuid4()), + HTTP_X_CHANNEL_TOKEN="some-token", + ) + assert response.status_code == 401 + + def test_inactive_channel_id(self): + """Inactive channel should return 401.""" + user = factories.UserFactory() + channel = factories.ChannelFactory( + user=user, + settings={"role": "reader"}, + is_active=False, + ) + token = channel.encrypted_settings["token"] + + client = APIClient() + response = client.generic( + "PROPFIND", + f"/caldav/calendars/users/{user.email}/", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + ) + assert response.status_code == 401 + + @patch("core.api.viewsets_caldav.CalDAVHTTPClient") + def test_caldav_path_scoped_channel(self, mock_http_cls): + """Channel with caldav_path scope restricts to that path.""" + user = factories.UserFactory() + scoped_path = f"/calendars/users/{user.email}/specific-cal/" + channel = factories.ChannelFactory( + user=user, + settings={"role": "reader"}, + caldav_path=scoped_path, + ) + token = channel.encrypted_settings["token"] + + mock_response = type( + "R", + (), + { + "status_code": 207, + "content": b"", + "headers": {"Content-Type": "application/xml"}, + }, + )() + mock_http_cls.build_base_headers.return_value = { + "X-Api-Key": "test", + "X-Forwarded-User": user.email, + } + + client = APIClient() + + # Allowed: within scoped path + with patch( + "core.api.viewsets_caldav.requests.request", return_value=mock_response + ): + response = client.generic( + "PROPFIND", + f"/caldav{scoped_path}", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + HTTP_DEPTH="1", + ) + assert response.status_code == 207 + + # Denied: different calendar + response = client.generic( + "PROPFIND", + f"/caldav/calendars/users/{user.email}/other-cal/", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + ) + assert response.status_code == 403 + + def test_caldav_path_boundary_no_prefix_leak(self): + """Scoped path /cal1/ must NOT match /cal1-secret/ (trailing slash boundary).""" + user = factories.UserFactory() + scoped_path = f"/calendars/users/{user.email}/cal1/" + channel = factories.ChannelFactory( + user=user, + settings={"role": "reader"}, + caldav_path=scoped_path, + ) + token = channel.encrypted_settings["token"] + + client = APIClient() + response = client.generic( + "PROPFIND", + f"/caldav/calendars/users/{user.email}/cal1-secret/", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + ) + assert response.status_code == 403 + + @patch("core.api.viewsets_caldav.get_user_entitlements") + def test_channel_mkcalendar_checks_entitlements(self, mock_entitlements): + """MKCALENDAR via channel token must still check entitlements.""" + mock_entitlements.return_value = {"can_access": False} + + user = factories.UserFactory() + channel = factories.ChannelFactory( + user=user, + settings={"role": "admin"}, + ) + token = channel.encrypted_settings["token"] + + client = APIClient() + response = client.generic( + "MKCALENDAR", + f"/caldav/calendars/users/{user.email}/new-cal/", + HTTP_X_CHANNEL_ID=str(channel.pk), + HTTP_X_CHANNEL_TOKEN=token, + ) + assert response.status_code == 403 + mock_entitlements.assert_called_once_with(user.sub, user.email) diff --git a/src/backend/core/tests/test_cross_org_e2e.py b/src/backend/core/tests/test_cross_org_e2e.py new file mode 100644 index 0000000..8539808 --- /dev/null +++ b/src/backend/core/tests/test_cross_org_e2e.py @@ -0,0 +1,709 @@ +"""End-to-end cross-organization isolation tests against real SabreDAV. + +These tests verify that org-scoped resources, calendars, and operations +are properly isolated between organizations. They hit the real SabreDAV +server (no mocks) to validate the full stack: Django -> SabreDAV -> DB. + +Requires: CalDAV server running (skipped otherwise). +""" + +# pylint: disable=no-member,broad-exception-caught,unused-variable + +from datetime import datetime, timedelta +from types import SimpleNamespace + +import pytest +from rest_framework.status import ( + HTTP_201_CREATED, + HTTP_204_NO_CONTENT, + HTTP_207_MULTI_STATUS, + HTTP_400_BAD_REQUEST, +) +from rest_framework.test import APIClient + +from core import factories +from core.entitlements.factory import get_entitlements_backend +from core.models import Organization, User +from core.services.caldav_service import CalDAVHTTPClient, CalendarService +from core.services.resource_service import ResourceProvisioningError, ResourceService + +pytestmark = [ + pytest.mark.django_db, + pytest.mark.xdist_group("caldav"), +] + + +@pytest.fixture(autouse=True) +def _local_entitlements(settings): + """Use local entitlements backend for all tests in this module.""" + settings.ENTITLEMENTS_BACKEND = ( + "core.entitlements.backends.local.LocalEntitlementsBackend" + ) + settings.ENTITLEMENTS_BACKEND_PARAMETERS = {} + get_entitlements_backend.cache_clear() + yield + get_entitlements_backend.cache_clear() + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _create_org_admin(org): + """Create a user in the given org and return (user, api_client). + + Uses force_login (not force_authenticate) so that session-based auth + works for the CalDAV proxy view, which checks request.user.is_authenticated + via Django's session middleware rather than DRF's token auth. + """ + user = factories.UserFactory(organization=org) + client = APIClient() + client.force_login(user) + return user, client + + +def _create_resource_via_internal_api(user, name="Room 1", resource_type="ROOM"): + """Create a resource using ResourceService (hits real SabreDAV).""" + service = ResourceService() + return service.create_resource(user, name, resource_type) + + +def _get_dav_client_with_org(user): + """Get a DAVClient with org header via CalDAVHTTPClient.""" + http = CalDAVHTTPClient() + return http.get_dav_client(user) + + +def _propfind_resource_principals(api_client): + """PROPFIND /caldav/principals/resources/ and return parsed XML root.""" + body = ( + '' + '' + "" + "" + "" + "" + "" + ) + response = api_client.generic( + "PROPFIND", + "/caldav/principals/resources/", + data=body, + content_type="application/xml", + HTTP_DEPTH="1", + ) + return response + + +def _propfind_resource_calendar(api_client, resource_id): + """PROPFIND a specific resource's calendar collection.""" + body = ( + '' + '' + ) + response = api_client.generic( + "PROPFIND", + f"/caldav/calendars/resources/{resource_id}/", + data=body, + content_type="application/xml", + HTTP_DEPTH="1", + ) + return response + + +def _put_event_on_resource(api_client, resource_id, event_uid, organizer_email): + """PUT an event directly onto a resource's default calendar.""" + dtstart = datetime.now() + timedelta(days=1) + dtend = dtstart + timedelta(hours=1) + ical = ( + "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//Test//Test//EN\r\n" + "BEGIN:VEVENT\r\n" + f"UID:{event_uid}\r\n" + f"DTSTART:{dtstart.strftime('%Y%m%dT%H%M%SZ')}\r\n" + f"DTEND:{dtend.strftime('%Y%m%dT%H%M%SZ')}\r\n" + "SUMMARY:Cross-org test event\r\n" + f"ORGANIZER:mailto:{organizer_email}\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR\r\n" + ) + return api_client.generic( + "PUT", + f"/caldav/calendars/resources/{resource_id}/default/{event_uid}.ics", + data=ical, + content_type="text/calendar", + ) + + +# --------------------------------------------------------------------------- +# 1. Resource provisioning — cross-org isolation via real SabreDAV +# --------------------------------------------------------------------------- + + +class TestResourceProvisioningE2E: + """Resource creation and deletion hit real SabreDAV internal API.""" + + def test_create_resource_e2e(self): + """POST /resources/ creates a principal in SabreDAV.""" + org = factories.OrganizationFactory(external_id="res-e2e-org") + admin, client = _create_org_admin(org) + + response = client.post( + "/api/v1.0/resources/", + {"name": "Meeting Room A", "resource_type": "ROOM"}, + format="json", + ) + + assert response.status_code == HTTP_201_CREATED, response.json() + data = response.json() + assert data["name"] == "Meeting Room A" + assert data["resource_type"] == "ROOM" + resource_id = data["id"] + + # Verify the principal actually exists in SabreDAV via PROPFIND + propfind = _propfind_resource_calendar(client, resource_id) + assert propfind.status_code == HTTP_207_MULTI_STATUS, ( + f"Resource calendar not found in SabreDAV: {propfind.status_code}" + ) + + def test_delete_resource_e2e_same_org(self): + """Admin can delete a resource belonging to their own org.""" + org = factories.OrganizationFactory(external_id="del-same-org") + admin, client = _create_org_admin(org) + + resource = _create_resource_via_internal_api(admin, "Doomed Room") + resource_id = resource["id"] + + response = client.delete(f"/api/v1.0/resources/{resource_id}/") + assert response.status_code == HTTP_204_NO_CONTENT + + # Verify the principal is gone from SabreDAV + propfind = _propfind_resource_calendar(client, resource_id) + # Should be 404 or 207 with empty result — the principal was deleted + assert propfind.status_code != HTTP_207_MULTI_STATUS or ( + b"" not in propfind.content + ) + + def test_delete_resource_e2e_cross_org_blocked(self): + """Admin from org A CANNOT delete a resource belonging to org B. + + This is enforced by SabreDAV's InternalApiPlugin, not Django. + """ + org_a = factories.OrganizationFactory(external_id="del-org-a") + org_b = factories.OrganizationFactory(external_id="del-org-b") + + # Create resource in org B + admin_b = factories.UserFactory(organization=org_b) + resource = _create_resource_via_internal_api(admin_b, "Org B Room") + resource_id = resource["id"] + + # Admin from org A tries to delete it + _, client_a = _create_org_admin(org_a) + response = client_a.delete(f"/api/v1.0/resources/{resource_id}/") + + # Django returns 400 because SabreDAV returned 403 + assert response.status_code == HTTP_400_BAD_REQUEST + assert "different organization" in response.json()["detail"].lower() + + +# --------------------------------------------------------------------------- +# 2. CalDAV proxy — org header forwarding verified E2E +# --------------------------------------------------------------------------- + + +class TestCalDAVProxyOrgHeaderE2E: + """Verify org header reaches SabreDAV and affects responses.""" + + def test_user_can_propfind_own_calendar(self): + """User can PROPFIND their own calendar home.""" + org = factories.OrganizationFactory(external_id="proxy-own") + user, client = _create_org_admin(org) + + # Create a calendar for the user + service = CalendarService() + service.create_calendar(user, name="My Cal") + + response = client.generic( + "PROPFIND", + f"/caldav/calendars/users/{user.email}/", + data='' + "", + content_type="application/xml", + HTTP_DEPTH="1", + ) + + assert response.status_code == HTTP_207_MULTI_STATUS + + def test_user_cannot_read_other_users_calendar_objects(self): + """User from org A cannot read events from user B's calendar. + + SabreDAV allows PROPFIND on calendar homes (auto-creates principals), + but blocks reading actual calendar objects via ACLs. The key isolation + is that event data (REPORT/GET on .ics) is protected. + """ + org_a = factories.OrganizationFactory(external_id="proxy-org-a") + org_b = factories.OrganizationFactory(external_id="proxy-org-b") + user_a, client_a = _create_org_admin(org_a) + user_b = factories.UserFactory(organization=org_b) + + # Create a calendar with an event for user B + service = CalendarService() + caldav_path = service.create_calendar(user_b, name="B's Calendar") + parts = caldav_path.strip("/").split("/") + cal_id = parts[-1] if len(parts) >= 4 else "default" + + # Add an event to user B's calendar + dav_b = CalDAVHTTPClient().get_dav_client(user_b) + principal_b = dav_b.principal() + cals_b = principal_b.calendars() + dtstart = datetime.now() + timedelta(days=10) + dtend = dtstart + timedelta(hours=1) + ical = ( + "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//Test//Test//EN\r\n" + "BEGIN:VEVENT\r\n" + "UID:private-event-uid\r\n" + f"DTSTART:{dtstart.strftime('%Y%m%dT%H%M%SZ')}\r\n" + f"DTEND:{dtend.strftime('%Y%m%dT%H%M%SZ')}\r\n" + "SUMMARY:Private Event\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR\r\n" + ) + cals_b[0].save_event(ical) + + # User A tries to GET user B's event + response = client_a.generic( + "GET", + f"/caldav/calendars/users/{user_b.email}/{cal_id}/private-event-uid.ics", + ) + + # SabreDAV should block with 403 (ACL) or 404 + assert response.status_code in (403, 404), ( + f"Expected 403/404 for cross-user GET, got {response.status_code}" + ) + + +# --------------------------------------------------------------------------- +# 3. Resource calendar access — cross-org isolation +# --------------------------------------------------------------------------- + + +class TestResourceCalendarAccessE2E: + """Verify that resource calendar data is org-scoped in SabreDAV. + + Users cannot PUT events directly on resource calendars — only + SabreDAV's scheduling plugin writes there via iTIP. These tests + verify that direct PUT is blocked by ACLs for both same-org and + cross-org users. + """ + + def test_direct_put_on_resource_calendar_blocked(self): + """Users cannot PUT events directly on resource calendars. + + Resource calendars are managed by the auto-schedule plugin. + Direct writes are blocked by SabreDAV ACLs. + """ + org = factories.OrganizationFactory(external_id="put-direct-org") + user, client = _create_org_admin(org) + resource = _create_resource_via_internal_api(user, "ACL Room") + + response = _put_event_on_resource( + client, resource["id"], "direct-put-uid", user.email + ) + + # Direct PUT on resource calendar is blocked by ACLs + assert response.status_code in (403, 404), ( + f"Expected 403/404 for direct PUT on resource calendar, " + f"got {response.status_code}: " + f"{response.content.decode('utf-8', errors='ignore')[:500]}" + ) + + def test_cross_org_put_on_resource_calendar_blocked(self): + """Cross-org direct PUT on resource calendar is also blocked.""" + org_a = factories.OrganizationFactory(external_id="put-org-a") + org_b = factories.OrganizationFactory(external_id="put-org-b") + _, client_a = _create_org_admin(org_a) + admin_b = factories.UserFactory(organization=org_b) + resource_b = _create_resource_via_internal_api(admin_b, "Org B Room") + + response = _put_event_on_resource( + client_a, resource_b["id"], "cross-org-event-uid", "attacker@test.com" + ) + + assert response.status_code in (403, 404), ( + f"Expected 403/404 for cross-org PUT on resource calendar, " + f"got {response.status_code}" + ) + + +# --------------------------------------------------------------------------- +# 4. Resource auto-scheduling — cross-org booking rejection +# --------------------------------------------------------------------------- + + +class TestResourceAutoScheduleCrossOrgE2E: + """Verify that cross-org resource bookings are declined by SabreDAV. + + The ResourceAutoSchedulePlugin checks X-CalDAV-Organization against + the resource's org_id and declines cross-org booking requests. + + NOTE: SabreDAV's scheduling plugin resolves attendees via the principal + backend. Resource principals live under principals/resources/, and the + scheduling plugin must find them by email for iTIP delivery to work. + If SCHEDULE-STATUS=5.x appears, it means the principal wasn't resolved, + which blocks both auto-accept and auto-decline. These tests verify the + org-scoping logic in InternalApiPlugin (create/delete) rather than + iTIP scheduling, since iTIP requires searchPrincipals() support for + the resources collection. + """ + + def test_resource_create_stores_org_id(self): + """Resource creation stores org_id in SabreDAV for later scoping.""" + org = factories.OrganizationFactory(external_id="sched-orgid") + user, _ = _create_org_admin(org) + resource = _create_resource_via_internal_api(user, "Org Room") + + # Verify the resource exists by PROPFIND on its calendar + propfind = _propfind_resource_calendar( + _create_org_admin(org)[1], resource["id"] + ) + assert propfind.status_code == HTTP_207_MULTI_STATUS + + def test_resource_delete_cross_org_rejected_by_sabredav(self): + """SabreDAV's InternalApiPlugin rejects cross-org resource deletion. + + This is the core org-scoping enforcement: the org_id stored on creation + is checked on deletion. + """ + org_a = factories.OrganizationFactory(external_id="sched-del-a") + org_b = factories.OrganizationFactory(external_id="sched-del-b") + admin_a = factories.UserFactory(organization=org_a) + admin_b = factories.UserFactory(organization=org_b) + + # Create resource in org B + resource = _create_resource_via_internal_api(admin_b, "Org B Room") + + # Attempt delete from org A — should fail + service = ResourceService() + with pytest.raises(ResourceProvisioningError, match="different organization"): + service.delete_resource(admin_a, resource["id"]) + + # Verify resource still exists + _, client_b = _create_org_admin(org_b) + propfind = _propfind_resource_calendar(client_b, resource["id"]) + assert propfind.status_code == HTTP_207_MULTI_STATUS + + +# --------------------------------------------------------------------------- +# 5. Resource principal discovery — all orgs see resource list +# but data is org-scoped +# --------------------------------------------------------------------------- + + +class TestResourceDiscoveryE2E: + """Verify resource principal discovery behavior across orgs.""" + + def test_resource_principals_visible_to_authenticated_users(self): + """Any authenticated user can PROPFIND /principals/resources/. + + ResourcePrincipal grants {DAV:}read to {DAV:}authenticated. + This allows resource discovery for scheduling. + """ + org = factories.OrganizationFactory(external_id="disc-org") + user, client = _create_org_admin(org) + resource = _create_resource_via_internal_api(user, "Discoverable Room") + + response = _propfind_resource_principals(client) + assert response.status_code == HTTP_207_MULTI_STATUS + + # The response should contain the resource we just created + content = response.content.decode("utf-8", errors="ignore") + assert resource["id"] in content or "Discoverable Room" in content + + +# --------------------------------------------------------------------------- +# 6. User deletion cleanup — real SabreDAV +# --------------------------------------------------------------------------- + + +class TestUserDeletionCleanupE2E: + """Verify user deletion cleans up CalDAV data in SabreDAV.""" + + def test_deleting_user_removes_caldav_principal(self): + """When a Django user is deleted, their SabreDAV principal is cleaned up.""" + user = factories.UserFactory(email="doomed-user@test-e2e.com") + + # Create a calendar for the user (creates principal in SabreDAV) + service = CalendarService() + service.create_calendar(user, name="Soon Deleted") + + # Verify calendar exists + dav = CalDAVHTTPClient().get_dav_client(user) + principal = dav.principal() + assert len(principal.calendars()) > 0 + + # Capture org before delete (Python obj persists but be explicit) + org_id = user.organization_id + + # Delete the user (signal triggers CalDAV cleanup) + user.delete() + + # Verify the principal's calendars are gone + # After deletion, the principal shouldn't exist, but due to + # auto-create behavior, just check calendars are empty + ghost = SimpleNamespace( + email="doomed-user@test-e2e.com", organization_id=org_id + ) + dav2 = CalDAVHTTPClient().get_dav_client(ghost) + try: + principal2 = dav2.principal() + cals = principal2.calendars() + # Either no calendars or the principal doesn't exist + assert len(cals) == 0, ( + f"Expected 0 calendars after deletion, found {len(cals)}" + ) + except Exception: # noqa: BLE001 + # Principal not found — expected + pass + + +# --------------------------------------------------------------------------- +# 7. Organization deletion cleanup — real SabreDAV +# --------------------------------------------------------------------------- + + +class TestOrgDeletionCleanupE2E: + """Verify org deletion cleans up all member CalDAV data.""" + + def test_deleting_org_removes_all_member_caldav_data(self): + """Deleting an org cleans up CalDAV data for all its members.""" + org = factories.OrganizationFactory(external_id="doomed-org-e2e") + alice = factories.UserFactory( + email="alice-doomed@test-e2e.com", organization=org + ) + bob = factories.UserFactory(email="bob-doomed@test-e2e.com", organization=org) + + # Create calendars for both users + service = CalendarService() + service.create_calendar(alice, name="Alice Cal") + service.create_calendar(bob, name="Bob Cal") + + # Verify calendars exist + for user in [alice, bob]: + dav = CalDAVHTTPClient().get_dav_client(user) + assert len(dav.principal().calendars()) > 0 + + # Capture org_id before deletion + org_id = org.id + + # Delete the org (cascades to member cleanup + user deletion) + org.delete() + + # Verify both users' calendars are gone + emails = ["alice-doomed@test-e2e.com", "bob-doomed@test-e2e.com"] + for email in emails: + ghost = SimpleNamespace(email=email, organization_id=org_id) + dav = CalDAVHTTPClient().get_dav_client(ghost) + try: + cals = dav.principal().calendars() + assert len(cals) == 0 + except Exception: # noqa: BLE001 + pass # Principal not found — expected + + assert not User.objects.filter( + email__in=[ + "alice-doomed@test-e2e.com", + "bob-doomed@test-e2e.com", + ] + ).exists() + assert not Organization.objects.filter(external_id="doomed-org-e2e").exists() + + +# --------------------------------------------------------------------------- +# 8. Calendar creation isolation +# --------------------------------------------------------------------------- + + +class TestCalendarCreationIsolationE2E: + """Verify calendar creation is scoped to the authenticated user.""" + + def test_mkcalendar_creates_for_authenticated_user_only(self): + """MKCALENDAR via proxy creates calendar under the authenticated user's principal.""" + org = factories.OrganizationFactory(external_id="mkcal-org") + user_a, client_a = _create_org_admin(org) + user_b = factories.UserFactory(organization=org) + + # User A creates a calendar via proxy + response = client_a.generic( + "MKCALENDAR", + f"/caldav/calendars/users/{user_a.email}/new-cal-e2e/", + data=( + '' + '' + "" + "E2E Test Calendar" + "" + "" + ), + content_type="application/xml", + ) + + assert response.status_code == 201, ( + f"MKCALENDAR failed: {response.status_code} " + f"{response.content.decode('utf-8', errors='ignore')[:500]}" + ) + + # Verify user A has the calendar via CalDAV PROPFIND + dav_a = CalDAVHTTPClient().get_dav_client(user_a) + cal_names_a = [c.name for c in dav_a.principal().calendars()] + assert "E2E Test Calendar" in cal_names_a, ( + f"Calendar 'E2E Test Calendar' not found for user A. Found: {cal_names_a}" + ) + + # Verify user B does NOT have this calendar + dav_b = CalDAVHTTPClient().get_dav_client(user_b) + try: + cal_names_b = [c.name for c in dav_b.principal().calendars()] + assert "E2E Test Calendar" not in cal_names_b + except Exception: # noqa: BLE001 + pass # User B has no principal — that's fine + + def test_cannot_create_calendar_under_other_user(self): + """User A cannot MKCALENDAR under user B's principal.""" + org = factories.OrganizationFactory(external_id="mkcal-cross") + user_a, client_a = _create_org_admin(org) + user_b = factories.UserFactory( + email="mkcal-victim@test-e2e.com", organization=org + ) + + response = client_a.generic( + "MKCALENDAR", + f"/caldav/calendars/users/{user_b.email}/hijacked-cal/", + data=( + '' + '' + "" + "Hijacked Calendar" + "" + "" + ), + content_type="application/xml", + ) + + # SabreDAV should block this with 403 (ACL violation) + assert response.status_code in (403, 404, 409), ( + f"Expected 403/404 for MKCALENDAR under another user, " + f"got {response.status_code}" + ) + + +# --------------------------------------------------------------------------- +# 9. Event CRUD isolation +# --------------------------------------------------------------------------- + + +class TestEventCRUDIsolationE2E: + """Verify event CRUD is scoped to the calendar owner.""" + + def test_user_cannot_put_event_in_other_users_calendar(self): + """User A cannot PUT an event into user B's calendar.""" + org = factories.OrganizationFactory(external_id="event-cross") + user_a, client_a = _create_org_admin(org) + user_b = factories.UserFactory( + email="event-victim@test-e2e.com", organization=org + ) + + # Create a calendar for user B + service = CalendarService() + caldav_path = service.create_calendar(user_b, name="B's Private Cal") + + # Extract calendar ID from path + # Path format: calendars/users/email/cal-id/ + parts = caldav_path.strip("/").split("/") + cal_id = parts[-1] if len(parts) >= 4 else "default" + + dtstart = datetime.now() + timedelta(days=5) + dtend = dtstart + timedelta(hours=1) + ical = ( + "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//Test//Test//EN\r\n" + "BEGIN:VEVENT\r\n" + "UID:malicious-event-uid\r\n" + f"DTSTART:{dtstart.strftime('%Y%m%dT%H%M%SZ')}\r\n" + f"DTEND:{dtend.strftime('%Y%m%dT%H%M%SZ')}\r\n" + "SUMMARY:Malicious Event\r\n" + f"ORGANIZER:mailto:{user_a.email}\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR\r\n" + ) + + response = client_a.generic( + "PUT", + f"/caldav/calendars/users/{user_b.email}/{cal_id}/malicious.ics", + data=ical, + content_type="text/calendar", + ) + + # SabreDAV should block with 403 (ACL) + assert response.status_code in (403, 404, 409), ( + f"Expected 403/404 for cross-user PUT, got {response.status_code}" + ) + + def test_user_cannot_delete_other_users_event(self): + """User A cannot DELETE an event from user B's calendar.""" + org = factories.OrganizationFactory(external_id="event-del-cross") + user_a, client_a = _create_org_admin(org) + user_b = factories.UserFactory( + email="event-del-victim@test-e2e.com", organization=org + ) + + # Create a calendar and event for user B + service = CalendarService() + caldav_path = service.create_calendar(user_b, name="B's Cal") + parts = caldav_path.strip("/").split("/") + cal_id = parts[-1] if len(parts) >= 4 else "default" + + # Create event as user B + dav_b = CalDAVHTTPClient().get_dav_client(user_b) + principal_b = dav_b.principal() + cals_b = principal_b.calendars() + assert len(cals_b) > 0 + + dtstart = datetime.now() + timedelta(days=6) + dtend = dtstart + timedelta(hours=1) + ical = ( + "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//Test//Test//EN\r\n" + "BEGIN:VEVENT\r\n" + "UID:victim-event-uid\r\n" + f"DTSTART:{dtstart.strftime('%Y%m%dT%H%M%SZ')}\r\n" + f"DTEND:{dtend.strftime('%Y%m%dT%H%M%SZ')}\r\n" + "SUMMARY:Victim's Event\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR\r\n" + ) + cals_b[0].save_event(ical) + + # User A tries to DELETE user B's event + response = client_a.generic( + "DELETE", + f"/caldav/calendars/users/{user_b.email}/{cal_id}/victim-event-uid.ics", + ) + + assert response.status_code in (403, 404), ( + f"Expected 403/404 for cross-user DELETE, got {response.status_code}" + ) + + # Verify event still exists via CalDAV API + http = CalDAVHTTPClient() + ical_data, _, _ = http.find_event_by_uid(user_b, "victim-event-uid") + assert ical_data is not None, ( + "Victim's event should still exist after blocked deletion attempt" + ) diff --git a/src/backend/core/tests/test_entitlements.py b/src/backend/core/tests/test_entitlements.py index 82a6da8..e633a59 100644 --- a/src/backend/core/tests/test_entitlements.py +++ b/src/backend/core/tests/test_entitlements.py @@ -27,10 +27,10 @@ from core.entitlements.factory import get_entitlements_backend def test_local_backend_always_grants_access(): - """The local backend should always return can_access=True.""" + """The local backend should always return can_access=True and can_admin=True.""" backend = LocalEntitlementsBackend() result = backend.get_user_entitlements("sub-123", "user@example.com") - assert result == {"can_access": True} + assert result == {"can_access": True, "can_admin": True} def test_local_backend_ignores_parameters(): @@ -42,7 +42,7 @@ def test_local_backend_ignores_parameters(): user_info={"some": "claim"}, force_refresh=True, ) - assert result == {"can_access": True} + assert result == {"can_access": True, "can_admin": True} # -- Factory -- @@ -99,7 +99,7 @@ def test_deploycenter_backend_grants_access(): responses.add( responses.GET, DC_URL, - json={"entitlements": {"can_access": True}}, + json={"entitlements": {"can_access": True, "can_admin": True}}, status=200, ) @@ -109,7 +109,7 @@ def test_deploycenter_backend_grants_access(): api_key="test-key", ) result = backend.get_user_entitlements("sub-123", "user@example.com") - assert result == {"can_access": True} + assert result == {"can_access": True, "can_admin": True} # Verify request was made with correct params and header assert len(responses.calls) == 1 @@ -135,7 +135,7 @@ def test_deploycenter_backend_denies_access(): api_key="test-key", ) result = backend.get_user_entitlements("sub-123", "user@example.com") - assert result == {"can_access": False} + assert result == {"can_access": False, "can_admin": False} @responses.activate @@ -157,12 +157,12 @@ def test_deploycenter_backend_uses_cache(): # First call hits the API result1 = backend.get_user_entitlements("sub-123", "user@example.com") - assert result1 == {"can_access": True} + assert result1["can_access"] is True assert len(responses.calls) == 1 # Second call should use cache result2 = backend.get_user_entitlements("sub-123", "user@example.com") - assert result2 == {"can_access": True} + assert result2["can_access"] is True assert len(responses.calls) == 1 # No additional API call @@ -230,7 +230,7 @@ def test_deploycenter_backend_fallback_to_stale_cache(): result = backend.get_user_entitlements( "sub-123", "user@example.com", force_refresh=True ) - assert result == {"can_access": True} + assert result["can_access"] is True @responses.activate @@ -355,20 +355,21 @@ def test_user_me_serializer_includes_can_access_false(): assert data["can_access"] is False -def test_user_me_serializer_can_access_fail_open(): - """UserMeSerializer should return can_access=True when entitlements unavailable.""" +def test_user_me_serializer_can_access_fail_closed(): + """UserMeSerializer should return can_access=False when entitlements unavailable.""" user = factories.UserFactory() with mock.patch( "core.api.serializers.get_user_entitlements", side_effect=EntitlementsUnavailableError("unavailable"), ): data = UserMeSerializer(user).data - assert data["can_access"] is True + assert data["can_access"] is False # -- Signals integration -- +@pytest.mark.xdist_group("caldav") @override_settings( CALDAV_URL="http://caldav:80", ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", @@ -392,6 +393,7 @@ def test_signal_skips_calendar_when_not_entitled(): get_entitlements_backend.cache_clear() +@pytest.mark.xdist_group("caldav") @override_settings( CALDAV_URL="http://caldav:80", ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", @@ -414,6 +416,7 @@ def test_signal_skips_calendar_when_entitlements_unavailable(): get_entitlements_backend.cache_clear() +@pytest.mark.xdist_group("caldav") @override_settings( CALDAV_URL="http://caldav:80", ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", @@ -456,7 +459,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member ): response = client.generic( "MKCALENDAR", - "/api/v1.0/caldav/calendars/test@example.com/new-cal/", + "/caldav/calendars/users/test@example.com/new-cal/", ) assert response.status_code == HTTP_403_FORBIDDEN @@ -474,7 +477,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member ): response = client.generic( "MKCOL", - "/api/v1.0/caldav/calendars/test@example.com/new-cal/", + "/caldav/calendars/users/test@example.com/new-cal/", ) assert response.status_code == HTTP_403_FORBIDDEN @@ -493,7 +496,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member ): response = client.generic( "MKCALENDAR", - "/api/v1.0/caldav/calendars/test@example.com/new-cal/", + "/caldav/calendars/users/test@example.com/new-cal/", ) assert response.status_code == HTTP_403_FORBIDDEN @@ -509,7 +512,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member responses.add( responses.Response( method="MKCALENDAR", - url=f"{caldav_url}/api/v1.0/caldav/calendars/test@example.com/new-cal/", + url=f"{caldav_url}/caldav/calendars/users/test@example.com/new-cal/", status=201, body="", ) @@ -521,7 +524,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member ): response = client.generic( "MKCALENDAR", - "/api/v1.0/caldav/calendars/test@example.com/new-cal/", + "/caldav/calendars/users/test@example.com/new-cal/", ) assert response.status_code == 201 @@ -538,7 +541,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member responses.add( responses.Response( method="PROPFIND", - url=f"{caldav_url}/api/v1.0/caldav/", + url=f"{caldav_url}/caldav/", status=HTTP_207_MULTI_STATUS, body='', headers={"Content-Type": "application/xml"}, @@ -546,7 +549,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member ) # No entitlements mock needed — PROPFIND should not check entitlements - response = client.generic("PROPFIND", "/api/v1.0/caldav/") + response = client.generic("PROPFIND", "/caldav/") assert response.status_code == HTTP_207_MULTI_STATUS @@ -561,7 +564,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member responses.add( responses.Response( method="REPORT", - url=f"{caldav_url}/api/v1.0/caldav/calendars/other@example.com/cal-id/", + url=f"{caldav_url}/caldav/calendars/users/other@example.com/cal-id/", status=HTTP_207_MULTI_STATUS, body='', headers={"Content-Type": "application/xml"}, @@ -570,7 +573,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member response = client.generic( "REPORT", - "/api/v1.0/caldav/calendars/other@example.com/cal-id/", + "/caldav/calendars/users/other@example.com/cal-id/", ) assert response.status_code == HTTP_207_MULTI_STATUS @@ -587,7 +590,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member responses.add( responses.Response( method="PUT", - url=f"{caldav_url}/api/v1.0/caldav/calendars/other@example.com/cal-id/event.ics", + url=f"{caldav_url}/caldav/calendars/users/other@example.com/cal-id/event.ics", status=HTTP_200_OK, body="", ) @@ -595,7 +598,7 @@ class TestCalDAVProxyEntitlements: # pylint: disable=no-member response = client.generic( "PUT", - "/api/v1.0/caldav/calendars/other@example.com/cal-id/event.ics", + "/caldav/calendars/users/other@example.com/cal-id/event.ics", data=b"BEGIN:VCALENDAR\nEND:VCALENDAR", content_type="text/calendar", ) diff --git a/src/backend/core/tests/test_ical_export.py b/src/backend/core/tests/test_ical_export.py index 4f92fae..485cba5 100644 --- a/src/backend/core/tests/test_ical_export.py +++ b/src/backend/core/tests/test_ical_export.py @@ -1,9 +1,6 @@ -"""Tests for iCal export endpoint.""" - -import uuid +"""Tests for iCal export endpoint (using Channel type=ical-feed).""" from django.conf import settings -from django.urls import reverse import pytest import responses @@ -11,22 +8,28 @@ from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_502_BAD_ from rest_framework.test import APIClient from core import factories +from core.models import uuid_to_urlsafe @pytest.mark.django_db class TestICalExport: """Tests for ICalExportView.""" + def _ical_url(self, channel): + """Build the iCal export URL for a channel.""" + token = channel.encrypted_settings["token"] + short_id = uuid_to_urlsafe(channel.pk) + return f"/ical/{short_id}/{token}/calendar.ics" + def test_export_with_valid_token_returns_ics(self): """Test that a valid token returns iCal data.""" - subscription = factories.CalendarSubscriptionTokenFactory() + channel = factories.ICalFeedChannelFactory() client = APIClient() - # Mock CalDAV server response with responses.RequestsMock() as rsps: caldav_url = settings.CALDAV_URL - caldav_path = subscription.caldav_path.lstrip("/") - target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export" + caldav_path = channel.caldav_path.lstrip("/") + target_url = f"{caldav_url}/caldav/{caldav_path}?export" ics_content = b"""BEGIN:VCALENDAR VERSION:2.0 @@ -47,8 +50,7 @@ END:VCALENDAR""" content_type="text/calendar", ) - url = reverse("ical-export", kwargs={"token": subscription.token}) - response = client.get(url) + response = client.get(self._ical_url(channel)) assert response.status_code == HTTP_200_OK assert response["Content-Type"] == "text/calendar; charset=utf-8" @@ -58,35 +60,38 @@ END:VCALENDAR""" def test_export_with_invalid_token_returns_404(self): """Test that an invalid token returns 404.""" + channel = factories.ICalFeedChannelFactory() + short_id = uuid_to_urlsafe(channel.pk) client = APIClient() - invalid_token = uuid.uuid4() - - url = reverse("ical-export", kwargs={"token": invalid_token}) - response = client.get(url) + response = client.get(f"/ical/{short_id}/WrongTokenHere123/calendar.ics") + assert response.status_code == HTTP_404_NOT_FOUND + def test_export_with_invalid_channel_id_returns_404(self): + """Test that a nonexistent channel ID returns 404.""" + client = APIClient() + # base62-encoded zero UUID + response = client.get("/ical/0/SomeToken123/calendar.ics") assert response.status_code == HTTP_404_NOT_FOUND def test_export_with_inactive_token_returns_404(self): """Test that an inactive token returns 404.""" - subscription = factories.CalendarSubscriptionTokenFactory(is_active=False) + channel = factories.ICalFeedChannelFactory(is_active=False) client = APIClient() - url = reverse("ical-export", kwargs={"token": subscription.token}) - response = client.get(url) - + response = client.get(self._ical_url(channel)) assert response.status_code == HTTP_404_NOT_FOUND - def test_export_updates_last_accessed_at(self): - """Test that accessing the export updates last_accessed_at.""" - subscription = factories.CalendarSubscriptionTokenFactory() - assert subscription.last_accessed_at is None + def test_export_updates_last_used_at(self): + """Test that accessing the export updates last_used_at.""" + channel = factories.ICalFeedChannelFactory() + assert channel.last_used_at is None client = APIClient() with responses.RequestsMock() as rsps: caldav_url = settings.CALDAV_URL - caldav_path = subscription.caldav_path.lstrip("/") - target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export" + caldav_path = channel.caldav_path.lstrip("/") + target_url = f"{caldav_url}/caldav/{caldav_path}?export" rsps.add( responses.GET, @@ -96,22 +101,20 @@ END:VCALENDAR""" content_type="text/calendar", ) - url = reverse("ical-export", kwargs={"token": subscription.token}) - client.get(url) + client.get(self._ical_url(channel)) - subscription.refresh_from_db() - assert subscription.last_accessed_at is not None + channel.refresh_from_db() + assert channel.last_used_at is not None def test_export_does_not_require_authentication(self): """Test that the endpoint is accessible without authentication.""" - subscription = factories.CalendarSubscriptionTokenFactory() + channel = factories.ICalFeedChannelFactory() client = APIClient() - # Not logging in - should still work with responses.RequestsMock() as rsps: caldav_url = settings.CALDAV_URL - caldav_path = subscription.caldav_path.lstrip("/") - target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export" + caldav_path = channel.caldav_path.lstrip("/") + target_url = f"{caldav_url}/caldav/{caldav_path}?export" rsps.add( responses.GET, @@ -121,20 +124,18 @@ END:VCALENDAR""" content_type="text/calendar", ) - url = reverse("ical-export", kwargs={"token": subscription.token}) - response = client.get(url) - + response = client.get(self._ical_url(channel)) assert response.status_code == HTTP_200_OK def test_export_sends_correct_headers_to_caldav(self): """Test that the proxy sends correct authentication headers to CalDAV.""" - subscription = factories.CalendarSubscriptionTokenFactory() + channel = factories.ICalFeedChannelFactory() client = APIClient() with responses.RequestsMock() as rsps: caldav_url = settings.CALDAV_URL - caldav_path = subscription.caldav_path.lstrip("/") - target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export" + caldav_path = channel.caldav_path.lstrip("/") + target_url = f"{caldav_url}/caldav/{caldav_path}?export" rsps.add( responses.GET, @@ -144,24 +145,22 @@ END:VCALENDAR""" content_type="text/calendar", ) - url = reverse("ical-export", kwargs={"token": subscription.token}) - client.get(url) + client.get(self._ical_url(channel)) - # Verify headers sent to CalDAV assert len(rsps.calls) == 1 request = rsps.calls[0].request - assert request.headers["X-Forwarded-User"] == subscription.owner.email + assert request.headers["X-Forwarded-User"] == channel.user.email assert request.headers["X-Api-Key"] == settings.CALDAV_OUTBOUND_API_KEY def test_export_handles_caldav_error(self): """Test that CalDAV server errors are handled gracefully.""" - subscription = factories.CalendarSubscriptionTokenFactory() + channel = factories.ICalFeedChannelFactory() client = APIClient() with responses.RequestsMock() as rsps: caldav_url = settings.CALDAV_URL - caldav_path = subscription.caldav_path.lstrip("/") - target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export" + caldav_path = channel.caldav_path.lstrip("/") + target_url = f"{caldav_url}/caldav/{caldav_path}?export" rsps.add( responses.GET, @@ -170,20 +169,18 @@ END:VCALENDAR""" status=500, ) - url = reverse("ical-export", kwargs={"token": subscription.token}) - response = client.get(url) - + response = client.get(self._ical_url(channel)) assert response.status_code == HTTP_502_BAD_GATEWAY def test_export_sets_security_headers(self): """Test that security headers are set correctly.""" - subscription = factories.CalendarSubscriptionTokenFactory() + channel = factories.ICalFeedChannelFactory() client = APIClient() with responses.RequestsMock() as rsps: caldav_url = settings.CALDAV_URL - caldav_path = subscription.caldav_path.lstrip("/") - target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export" + caldav_path = channel.caldav_path.lstrip("/") + target_url = f"{caldav_url}/caldav/{caldav_path}?export" rsps.add( responses.GET, @@ -193,24 +190,22 @@ END:VCALENDAR""" content_type="text/calendar", ) - url = reverse("ical-export", kwargs={"token": subscription.token}) - response = client.get(url) + response = client.get(self._ical_url(channel)) - # Verify security headers assert response["Cache-Control"] == "no-store, private" assert response["Referrer-Policy"] == "no-referrer" def test_export_uses_calendar_name_in_filename(self): - """Test that the export filename uses the calendar_name.""" - subscription = factories.CalendarSubscriptionTokenFactory( - calendar_name="My Test Calendar" + """Test that the export filename uses the calendar_name from settings.""" + channel = factories.ICalFeedChannelFactory( + settings={"role": "reader", "calendar_name": "My Test Calendar"} ) client = APIClient() with responses.RequestsMock() as rsps: caldav_url = settings.CALDAV_URL - caldav_path = subscription.caldav_path.lstrip("/") - target_url = f"{caldav_url}/api/v1.0/caldav/{caldav_path}?export" + caldav_path = channel.caldav_path.lstrip("/") + target_url = f"{caldav_url}/caldav/{caldav_path}?export" rsps.add( responses.GET, @@ -220,7 +215,15 @@ END:VCALENDAR""" content_type="text/calendar", ) - url = reverse("ical-export", kwargs={"token": subscription.token}) - response = client.get(url) + response = client.get(self._ical_url(channel)) + assert "my-test-calendar.ics" in response["Content-Disposition"] - assert "My Test Calendar.ics" in response["Content-Disposition"] + def test_non_ical_feed_channel_returns_404(self): + """Test that a valid token for a non-ical-feed channel returns 404.""" + channel = factories.ChannelFactory() # type="caldav" (default) + token = channel.encrypted_settings["token"] + short_id = uuid_to_urlsafe(channel.pk) + client = APIClient() + + response = client.get(f"/ical/{short_id}/{token}/calendar.ics") + assert response.status_code == HTTP_404_NOT_FOUND diff --git a/src/backend/core/tests/test_import_events.py b/src/backend/core/tests/test_import_events.py index acd0f02..886a6ff 100644 --- a/src/backend/core/tests/test_import_events.py +++ b/src/backend/core/tests/test_import_events.py @@ -247,7 +247,7 @@ END:VCALENDAR""" def _make_caldav_path(user): """Build a caldav_path string for a user (test helper).""" - return f"/calendars/{user.email}/{uuid.uuid4()}/" + return f"/calendars/users/{user.email}/{uuid.uuid4()}/" def _make_sabredav_response( # noqa: PLR0913 # pylint: disable=too-many-arguments,too-many-positional-arguments @@ -482,7 +482,7 @@ class TestICSImportService: @patch("core.services.caldav_service.requests.request") def test_import_passes_calendar_path(self, mock_post): - """The import URL should include the caldav_path.""" + """The import URL should use the internal-api/import/ endpoint.""" mock_post.return_value = _make_sabredav_response( total_events=1, imported_count=1 ) @@ -495,8 +495,11 @@ class TestICSImportService: call_args = mock_post.call_args url = call_args.args[0] if call_args.args else call_args.kwargs.get("url", "") - assert caldav_path in url - assert "?import" in url + assert "internal-api/import/" in url + # URL should contain the user email and calendar URI from the path + parts = caldav_path.strip("/").split("/") + assert parts[2] in url # user email + assert parts[3] in url # calendar URI @patch("core.services.caldav_service.requests.request") def test_import_sends_auth_headers(self, mock_post): @@ -515,7 +518,7 @@ class TestICSImportService: headers = call_kwargs["headers"] assert headers["X-Api-Key"] == settings.CALDAV_OUTBOUND_API_KEY assert headers["X-Forwarded-User"] == user.email - assert headers["X-Calendars-Import"] == settings.CALDAV_OUTBOUND_API_KEY + assert headers["X-Internal-Api-Key"] == settings.CALDAV_INTERNAL_API_KEY assert headers["Content-Type"] == "text/calendar" @patch("core.services.caldav_service.requests.request") @@ -571,7 +574,7 @@ class TestImportEventsAPI: """Users cannot import to a calendar they don't own.""" owner = factories.UserFactory(email="owner@example.com") other_user = factories.UserFactory(email="other@example.com") - caldav_path = f"/calendars/{owner.email}/some-uuid/" + caldav_path = f"/calendars/users/{owner.email}/some-uuid/" client = APIClient() client.force_login(other_user) @@ -597,12 +600,12 @@ class TestImportEventsAPI: ) response = client.post(self.IMPORT_URL, {"file": ics_file}, format="multipart") assert response.status_code == 400 - assert "caldav_path" in response.json()["error"] + assert "caldav_path" in response.json()["detail"] def test_import_events_missing_file(self): """Request without a file should return 400.""" user = factories.UserFactory(email="nofile@example.com") - caldav_path = f"/calendars/{user.email}/some-uuid/" + caldav_path = f"/calendars/users/{user.email}/some-uuid/" client = APIClient() client.force_login(user) @@ -613,12 +616,12 @@ class TestImportEventsAPI: format="multipart", ) assert response.status_code == 400 - assert "No file provided" in response.json()["error"] + assert "No file provided" in response.json()["detail"] def test_import_events_file_too_large(self): """Files exceeding MAX_FILE_SIZE should be rejected.""" user = factories.UserFactory(email="largefile@example.com") - caldav_path = f"/calendars/{user.email}/some-uuid/" + caldav_path = f"/calendars/users/{user.email}/some-uuid/" client = APIClient() client.force_login(user) @@ -634,11 +637,11 @@ class TestImportEventsAPI: format="multipart", ) assert response.status_code == 400 - assert "too large" in response.json()["error"] + assert "too large" in response.json()["detail"] @patch.object(ICSImportService, "import_events") - def test_import_events_success(self, mock_import): - """Successful import should return result data.""" + def test_import_events_returns_task_id(self, mock_import): + """Successful import should return a task_id for polling.""" mock_import.return_value = ImportResult( total_events=3, imported_count=3, @@ -648,7 +651,7 @@ class TestImportEventsAPI: ) user = factories.UserFactory(email="success@example.com") - caldav_path = f"/calendars/{user.email}/some-uuid/" + caldav_path = f"/calendars/users/{user.email}/some-uuid/" client = APIClient() client.force_login(user) @@ -662,51 +665,20 @@ class TestImportEventsAPI: format="multipart", ) - assert response.status_code == 200 + assert response.status_code == 202 data = response.json() - assert data["total_events"] == 3 - assert data["imported_count"] == 3 - assert data["skipped_count"] == 0 - assert "errors" not in data + assert "task_id" in data - @patch.object(ICSImportService, "import_events") - def test_import_events_partial_success(self, mock_import): - """Partial success should include errors in response.""" - mock_import.return_value = ImportResult( - total_events=3, - imported_count=2, - duplicate_count=0, - skipped_count=1, - errors=["Planning session"], - ) - - user = factories.UserFactory(email="partial@example.com") - caldav_path = f"/calendars/{user.email}/some-uuid/" - - client = APIClient() - client.force_login(user) - - ics_file = SimpleUploadedFile( - "events.ics", ICS_MULTIPLE_EVENTS, content_type="text/calendar" - ) - response = client.post( - self.IMPORT_URL, - {"file": ics_file, "caldav_path": caldav_path}, - format="multipart", - ) - - assert response.status_code == 200 - data = response.json() - assert data["total_events"] == 3 - assert data["imported_count"] == 2 - assert data["skipped_count"] == 1 - assert len(data["errors"]) == 1 + # With EagerBroker, the task runs synchronously — poll for result + task_response = client.get(f"/api/v1.0/tasks/{data['task_id']}/") + assert task_response.status_code == 200 + task_data = task_response.json() + assert task_data["status"] == "SUCCESS" + assert task_data["result"]["total_events"] == 3 + assert task_data["result"]["imported_count"] == 3 -@pytest.mark.skipif( - not settings.CALDAV_URL, - reason="CalDAV server URL not configured", -) +@pytest.mark.xdist_group("caldav") class TestImportEventsE2E: """End-to-end tests that import ICS events through the real SabreDAV server.""" @@ -827,11 +799,16 @@ class TestImportEventsE2E: format="multipart", ) - assert response.status_code == 200 - data = response.json() - assert data["total_events"] == 3 - assert data["imported_count"] == 3 - assert data["skipped_count"] == 0 + assert response.status_code == 202 + task_id = response.json()["task_id"] + + # With EagerBroker, poll for the synchronous result + task_response = client.get(f"/api/v1.0/tasks/{task_id}/") + assert task_response.status_code == 200 + data = task_response.json() + assert data["status"] == "SUCCESS" + assert data["result"]["total_events"] == 3 + assert data["result"]["imported_count"] == 3 # Verify events actually exist in SabreDAV caldav = CalDAVClient() @@ -955,7 +932,7 @@ class TestImportEventsE2E: """Fetch the raw ICS data of a single event from SabreDAV by UID.""" caldav_client = CalDAVClient() client = caldav_client._get_client(user) # pylint: disable=protected-access - cal_url = f"{caldav_client.base_url}{caldav_path}" + cal_url = caldav_client._calendar_url(caldav_path) # pylint: disable=protected-access cal = client.calendar(url=cal_url) event = cal.event_by_uid(uid) return event.data @@ -1022,10 +999,7 @@ class TestImportEventsE2E: assert "..." in raw -@pytest.mark.skipif( - not settings.CALDAV_URL, - reason="CalDAV server URL not configured", -) +@pytest.mark.xdist_group("caldav") class TestCalendarSanitizerE2E: """E2E tests for CalendarSanitizerPlugin on normal CalDAV PUT operations.""" @@ -1038,7 +1012,7 @@ class TestCalendarSanitizerE2E: """Fetch the raw ICS data of a single event from SabreDAV by UID.""" caldav_client = CalDAVClient() client = caldav_client._get_client(user) # pylint: disable=protected-access - cal_url = f"{caldav_client.base_url}{caldav_path}" + cal_url = caldav_client._calendar_url(caldav_path) # pylint: disable=protected-access cal = client.calendar(url=cal_url) event = cal.event_by_uid(uid) return event.data diff --git a/src/backend/core/tests/test_organizations.py b/src/backend/core/tests/test_organizations.py new file mode 100644 index 0000000..5979879 --- /dev/null +++ b/src/backend/core/tests/test_organizations.py @@ -0,0 +1,196 @@ +"""Tests for the organizations feature.""" + +from django.test import override_settings + +import pytest +from rest_framework.status import HTTP_200_OK +from rest_framework.test import APIClient + +from core import factories +from core.authentication.backends import resolve_organization +from core.entitlements.factory import get_entitlements_backend +from core.models import Organization + +# -- Organization model -- + + +@pytest.mark.django_db +def test_organization_str_with_name(): + """Organization.__str__ returns the name when set.""" + org = factories.OrganizationFactory(name="Acme Corp", external_id="acme") + assert str(org) == "Acme Corp" + + +@pytest.mark.django_db +def test_organization_str_without_name(): + """Organization.__str__ falls back to external_id when name is empty.""" + org = factories.OrganizationFactory(name="", external_id="acme") + assert str(org) == "acme" + + +@pytest.mark.django_db +def test_organization_unique_external_id(): + """external_id must be unique.""" + factories.OrganizationFactory(external_id="org-1") + with pytest.raises(Exception): # noqa: B017 + factories.OrganizationFactory(external_id="org-1") + + +# -- Org resolution on login -- + + +@pytest.mark.django_db +def test_resolve_org_from_email_domain(): + """Without OIDC_USERINFO_ORGANIZATION_CLAIM, org is derived from email domain.""" + user = factories.UserFactory(email="alice@ministry.gouv.fr") + + resolve_organization(user, claims={}, entitlements={}) + + user.refresh_from_db() + assert user.organization is not None + assert user.organization.external_id == "ministry.gouv.fr" + assert user.organization.name == "ministry.gouv.fr" + + +@pytest.mark.django_db +@override_settings(OIDC_USERINFO_ORGANIZATION_CLAIM="siret") +def test_resolve_org_from_oidc_claim(): + """With OIDC_USERINFO_ORGANIZATION_CLAIM, org is derived from the claim.""" + user = factories.UserFactory(email="alice@ministry.gouv.fr") + + resolve_organization( + user, + claims={"siret": "13002526500013"}, + entitlements={"organization_name": "Ministere X"}, + ) + + user.refresh_from_db() + assert user.organization is not None + assert user.organization.external_id == "13002526500013" + assert user.organization.name == "Ministere X" + + +@pytest.mark.django_db +def test_resolve_org_updates_name(): + """Org name is updated from entitlements on subsequent logins.""" + org = factories.OrganizationFactory(external_id="example.com", name="Old Name") + user = factories.UserFactory(email="alice@example.com", organization=org) + + resolve_organization( + user, + claims={}, + entitlements={"organization_name": "New Name"}, + ) + + org.refresh_from_db() + assert org.name == "New Name" + + +@pytest.mark.django_db +def test_resolve_org_reuses_existing(): + """Existing org is reused, not duplicated.""" + org = factories.OrganizationFactory(external_id="example.com") + user = factories.UserFactory(email="bob@example.com") + + resolve_organization(user, claims={}, entitlements={}) + + user.refresh_from_db() + assert user.organization_id == org.id + assert Organization.objects.filter(external_id="example.com").count() == 1 + + +# -- User API: /users/me/ -- + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, +) +def test_users_me_includes_organization(): + """GET /users/me/ includes the user's organization.""" + get_entitlements_backend.cache_clear() + org = factories.OrganizationFactory(name="Test Org", external_id="test") + user = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=user) + response = client.get("/api/v1.0/users/me/") + + assert response.status_code == HTTP_200_OK + data = response.json() + assert data["organization"]["id"] == str(org.id) + assert data["organization"]["name"] == "Test Org" + get_entitlements_backend.cache_clear() + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, +) +def test_users_me_includes_can_admin(): + """GET /users/me/ includes can_admin from entitlements.""" + get_entitlements_backend.cache_clear() + user = factories.UserFactory() + + client = APIClient() + client.force_authenticate(user=user) + response = client.get("/api/v1.0/users/me/") + + assert response.status_code == HTTP_200_OK + data = response.json() + assert "can_admin" in data + # Local backend returns True for can_admin + assert data["can_admin"] is True + get_entitlements_backend.cache_clear() + + +# -- User list scoping -- + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, +) +def test_user_list_scoped_by_org(): + """User list only returns users from the same org.""" + get_entitlements_backend.cache_clear() + org_a = factories.OrganizationFactory(external_id="org-a") + org_b = factories.OrganizationFactory(external_id="org-b") + + alice = factories.UserFactory(email="alice@example.com", organization=org_a) + factories.UserFactory(email="bob@other.com", organization=org_b) + + client = APIClient() + client.force_authenticate(user=alice) + response = client.get("/api/v1.0/users/?q=bob@other.com") + + assert response.status_code == HTTP_200_OK + # Bob should NOT be visible (different org) + assert len(response.json()["results"]) == 0 + get_entitlements_backend.cache_clear() + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, +) +def test_user_list_same_org_visible(): + """User list returns users from the same org.""" + get_entitlements_backend.cache_clear() + org = factories.OrganizationFactory(external_id="org-a") + alice = factories.UserFactory(email="alice@example.com", organization=org) + factories.UserFactory(email="carol@example.com", organization=org) + + client = APIClient() + client.force_authenticate(user=alice) + response = client.get("/api/v1.0/users/?q=carol@example.com") + + assert response.status_code == HTTP_200_OK + data = response.json()["results"] + assert len(data) == 1 + assert data[0]["email"] == "carol@example.com" + get_entitlements_backend.cache_clear() diff --git a/src/backend/core/tests/test_permissions_security.py b/src/backend/core/tests/test_permissions_security.py new file mode 100644 index 0000000..1abbf42 --- /dev/null +++ b/src/backend/core/tests/test_permissions_security.py @@ -0,0 +1,382 @@ +"""Tests for permissions, access control, and security edge cases.""" + +from unittest import mock + +from django.conf import settings +from django.test import override_settings + +import pytest +import responses +from rest_framework.status import ( + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_207_MULTI_STATUS, + HTTP_403_FORBIDDEN, +) +from rest_framework.test import APIClient + +from core import factories +from core.entitlements import EntitlementsUnavailableError +from core.entitlements.factory import get_entitlements_backend +from core.services.caldav_service import ( + CALDAV_PATH_PATTERN, + normalize_caldav_path, + verify_caldav_access, +) + +# --------------------------------------------------------------------------- +# verify_caldav_access — resource paths +# --------------------------------------------------------------------------- + + +class TestVerifyCaldavAccessResourcePaths: + """Tests for verify_caldav_access() with resource calendar paths.""" + + def _make_user(self, email="alice@example.com", org_id=None): + """Create a mock user object.""" + user = mock.Mock() + user.email = email + user.organization_id = org_id + return user + + def test_resource_path_allowed_when_user_has_org(self): + """Users with an organization can access resource calendars. + + Fine-grained org-to-resource authorization is enforced by SabreDAV + via the X-CalDAV-Organization header, not here. + """ + user = self._make_user(org_id="some-org-uuid") + path = "/calendars/resources/abc-123/default/" + assert verify_caldav_access(user, path) is True + + def test_user_path_allowed_for_own_email(self): + """Users can access their own calendar paths.""" + user = self._make_user(email="alice@example.com") + path = "/calendars/users/alice@example.com/cal-uuid/" + assert verify_caldav_access(user, path) is True + + def test_user_path_denied_for_other_email(self): + """Users cannot access another user's calendar path.""" + user = self._make_user(email="alice@example.com") + path = "/calendars/users/bob@example.com/cal-uuid/" + assert verify_caldav_access(user, path) is False + + def test_invalid_path_denied(self): + """Paths that don't match the expected pattern are rejected.""" + user = self._make_user(org_id="some-org") + assert verify_caldav_access(user, "/etc/passwd") is False + assert verify_caldav_access(user, "/calendars/") is False + assert verify_caldav_access(user, "/calendars/unknown/x/y/") is False + + def test_path_traversal_denied(self): + """Path traversal attempts are rejected.""" + user = self._make_user(email="alice@example.com") + path = "/calendars/users/alice@example.com/../../../etc/passwd/" + assert verify_caldav_access(user, path) is False + + def test_resource_path_pattern_matches(self): + """The CALDAV_PATH_PATTERN regex matches resource paths.""" + assert CALDAV_PATH_PATTERN.match("/calendars/resources/abc-123/default/") + assert CALDAV_PATH_PATTERN.match( + "/calendars/resources/a1b2c3d4-e5f6-7890-abcd-ef1234567890/default/" + ) + + def test_resource_path_denied_when_user_has_no_org(self): + """Users without an organization cannot access resource calendars.""" + user = self._make_user(org_id=None) + path = "/calendars/resources/abc-123/default/" + assert verify_caldav_access(user, path) is False + + def test_user_path_case_insensitive(self): + """Email comparison in user paths should be case-insensitive.""" + user = self._make_user(email="Alice@Example.COM") + path = "/calendars/users/alice@example.com/cal-uuid/" + assert verify_caldav_access(user, path) is True + + +# --------------------------------------------------------------------------- +# IsOrgAdmin permission +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +class TestIsOrgAdminPermission: + """Tests for the IsOrgAdmin permission class on the ResourceViewSet.""" + + @override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, + CALDAV_INTERNAL_API_KEY="test-internal-key", + ) + def test_admin_user_can_create_resource(self): + """Users with can_admin=True can create resources.""" + get_entitlements_backend.cache_clear() + org = factories.OrganizationFactory(external_id="test-org") + admin = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=admin) + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 201 + mock_response.text = '{"principal_uri": "principals/resources/x"}' + mock_request.return_value = mock_response + + response = client.post( + "/api/v1.0/resources/", + {"name": "Room 1"}, + format="json", + ) + + assert response.status_code == HTTP_201_CREATED + get_entitlements_backend.cache_clear() + + def test_non_admin_user_denied_resource_creation(self): + """Users with can_admin=False are denied by IsOrgAdmin.""" + org = factories.OrganizationFactory(external_id="test-org") + user = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=user) + + # Patch where IsOrgAdmin actually looks up get_user_entitlements + with mock.patch( + "core.api.permissions.get_user_entitlements", + return_value={"can_access": True, "can_admin": False}, + ): + response = client.post( + "/api/v1.0/resources/", + {"name": "Room 1"}, + format="json", + ) + + assert response.status_code == HTTP_403_FORBIDDEN + + def test_entitlements_unavailable_denies_access(self): + """IsOrgAdmin is fail-closed: denies when entitlements service is down.""" + org = factories.OrganizationFactory(external_id="test-org") + user = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=user) + + with mock.patch( + "core.api.permissions.get_user_entitlements", + side_effect=EntitlementsUnavailableError("Service unavailable"), + ): + response = client.post( + "/api/v1.0/resources/", + {"name": "Room 1"}, + format="json", + ) + + assert response.status_code == HTTP_403_FORBIDDEN + + def test_unauthenticated_user_denied(self): + """Unauthenticated users are denied by IsOrgAdmin (inherits IsAuthenticated).""" + client = APIClient() + response = client.post( + "/api/v1.0/resources/", + {"name": "Room 1"}, + format="json", + ) + # 401 or 403 depending on DRF config + assert response.status_code in (401, 403) + + def test_non_admin_user_denied_resource_deletion(self): + """Users with can_admin=False cannot delete resources either.""" + org = factories.OrganizationFactory(external_id="test-org") + user = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=user) + + with mock.patch( + "core.api.permissions.get_user_entitlements", + return_value={"can_access": True, "can_admin": False}, + ): + response = client.delete("/api/v1.0/resources/some-uuid/") + + assert response.status_code == HTTP_403_FORBIDDEN + + +# --------------------------------------------------------------------------- +# CalDAV proxy — X-CalDAV-Organization header forwarding +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +@pytest.mark.xdist_group("caldav") +class TestCalDAVProxyOrgHeader: + """Tests that the CalDAV proxy forwards the org header correctly.""" + + @responses.activate + def test_proxy_sends_org_header(self): + """CalDAV proxy sends X-CalDAV-Organization for users with an org.""" + org = factories.OrganizationFactory(external_id="org-alpha") + user = factories.UserFactory(email="alice@example.com", organization=org) + + client = APIClient() + client.force_login(user) + + caldav_url = settings.CALDAV_URL + responses.add( + responses.Response( + method="PROPFIND", + url=f"{caldav_url}/caldav/principals/resources/", + status=HTTP_207_MULTI_STATUS, + body='', + headers={"Content-Type": "application/xml"}, + ) + ) + + client.generic("PROPFIND", "/caldav/principals/resources/") + + assert len(responses.calls) == 1 + request = responses.calls[0].request + assert request.headers["X-CalDAV-Organization"] == str(org.id) + + @responses.activate + def test_proxy_cannot_spoof_org_header(self): + """Client-sent X-CalDAV-Organization is overwritten by the proxy.""" + org = factories.OrganizationFactory(external_id="real-org") + user = factories.UserFactory(email="alice@example.com", organization=org) + + client = APIClient() + client.force_login(user) + + caldav_url = settings.CALDAV_URL + responses.add( + responses.Response( + method="PROPFIND", + url=f"{caldav_url}/caldav/principals/resources/", + status=HTTP_207_MULTI_STATUS, + body='', + headers={"Content-Type": "application/xml"}, + ) + ) + + # Attempt to spoof the org header + client.generic( + "PROPFIND", + "/caldav/principals/resources/", + HTTP_X_CALDAV_ORGANIZATION="spoofed-org-id", + ) + + assert len(responses.calls) == 1 + request = responses.calls[0].request + # The proxy should use the user's real org ID, not the spoofed one + assert request.headers["X-CalDAV-Organization"] == str(org.id) + assert request.headers["X-CalDAV-Organization"] != "spoofed-org-id" + + +# --------------------------------------------------------------------------- +# IsEntitledToAccess permission — fail-closed +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +class TestIsEntitledToAccessFailClosed: + """Tests that IsEntitledToAccess permission is fail-closed.""" + + def test_import_denied_when_entitlements_unavailable(self): + """ICS import should be denied when entitlements service is down.""" + user = factories.UserFactory(email="alice@example.com") + + client = APIClient() + client.force_authenticate(user=user) + + with mock.patch( + "core.api.permissions.get_user_entitlements", + side_effect=EntitlementsUnavailableError("Service unavailable"), + ): + response = client.post( + "/api/v1.0/calendars/import-events/", + format="multipart", + ) + + assert response.status_code == HTTP_403_FORBIDDEN + + def test_import_denied_when_can_access_false(self): + """ICS import should be denied when can_access=False.""" + user = factories.UserFactory(email="alice@example.com") + + client = APIClient() + client.force_authenticate(user=user) + + with mock.patch( + "core.api.permissions.get_user_entitlements", + return_value={"can_access": False, "can_admin": False}, + ): + response = client.post( + "/api/v1.0/calendars/import-events/", + format="multipart", + ) + + assert response.status_code == HTTP_403_FORBIDDEN + + +# --------------------------------------------------------------------------- +# normalize_caldav_path +# --------------------------------------------------------------------------- + + +class TestNormalizeCaldavPath: + """Tests for normalize_caldav_path helper.""" + + def test_strips_old_api_prefix(self): + """Should strip any prefix before /calendars/.""" + result = normalize_caldav_path( + "/api/v1.0/caldav/calendars/users/user@ex.com/uuid/" + ) + assert result == "/calendars/users/user@ex.com/uuid/" + + def test_strips_new_prefix(self): + """Should strip /caldav prefix.""" + result = normalize_caldav_path("/caldav/calendars/users/user@ex.com/uuid/") + assert result == "/calendars/users/user@ex.com/uuid/" + + def test_adds_leading_slash(self): + """Should add a leading slash if missing.""" + result = normalize_caldav_path("calendars/users/user@ex.com/uuid/") + assert result == "/calendars/users/user@ex.com/uuid/" + + def test_adds_trailing_slash(self): + """Should add a trailing slash if missing.""" + result = normalize_caldav_path("/calendars/users/user@ex.com/uuid") + assert result == "/calendars/users/user@ex.com/uuid/" + + def test_resource_path_unchanged(self): + """Resource paths should pass through unchanged.""" + result = normalize_caldav_path("/calendars/resources/abc-123/default/") + assert result == "/calendars/resources/abc-123/default/" + + +# --------------------------------------------------------------------------- +# UserViewSet — user without org gets empty results +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +class TestUserViewSetCrossOrg: + """Tests that users from a different org see no users in the list.""" + + def test_user_from_other_org_gets_empty_list(self): + """A user from a different org should get an empty user list.""" + org_a = factories.OrganizationFactory(external_id="org-a") + org_b = factories.OrganizationFactory(external_id="org-b") + factories.UserFactory(email="orguser@example.com", organization=org_a) + + other_org_user = factories.UserFactory( + email="other@example.com", organization=org_b + ) + + client = APIClient() + client.force_login(other_org_user) + + response = client.get("/api/v1.0/users/", {"q": "orguser@example.com"}) + assert response.status_code == HTTP_200_OK + assert response.json()["count"] == 0 diff --git a/src/backend/core/tests/test_resources.py b/src/backend/core/tests/test_resources.py new file mode 100644 index 0000000..e7cf0b7 --- /dev/null +++ b/src/backend/core/tests/test_resources.py @@ -0,0 +1,309 @@ +"""Tests for the resource provisioning API.""" + +import json +from unittest import mock + +from django.test import override_settings + +import pytest +from rest_framework.status import ( + HTTP_201_CREATED, + HTTP_204_NO_CONTENT, + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED, +) +from rest_framework.test import APIClient + +from core import factories +from core.entitlements.factory import get_entitlements_backend +from core.services.resource_service import ResourceProvisioningError, ResourceService + +# -- Permission checks -- + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, +) +def test_create_resource_requires_auth(): + """POST /resources/ requires authentication.""" + get_entitlements_backend.cache_clear() + client = APIClient() + response = client.post( + "/api/v1.0/resources/", + {"name": "Room 1"}, + format="json", + ) + assert response.status_code == HTTP_401_UNAUTHORIZED + get_entitlements_backend.cache_clear() + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, + CALDAV_INTERNAL_API_KEY="test-internal-key", +) +def test_create_resource_success(): + """POST /resources/ creates a resource principal via the internal API.""" + get_entitlements_backend.cache_clear() + org = factories.OrganizationFactory(external_id="test-org") + admin = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=admin) + + with mock.patch("core.services.caldav_service.requests.request") as mock_request: + # Mock the internal API response for resource creation + mock_response = mock.Mock() + mock_response.status_code = 201 + mock_response.json.return_value = { + "principal_uri": "principals/resources/some-uuid", + "email": "c_test@resource.calendar.localhost", + } + mock_response.text = '{"principal_uri": "principals/resources/some-uuid"}' + mock_request.return_value = mock_response + + response = client.post( + "/api/v1.0/resources/", + {"name": "Room 101", "resource_type": "ROOM"}, + format="json", + ) + + assert response.status_code == HTTP_201_CREATED + data = response.json() + assert data["name"] == "Room 101" + assert data["resource_type"] == "ROOM" + assert "email" in data + assert "id" in data + # Principal URI uses the opaque UUID, not the slug + assert data["principal_uri"].startswith("principals/resources/") + assert data["principal_uri"] == f"principals/resources/{data['id']}" + + # Verify the HTTP call went to the internal API + mock_request.assert_called_once() + call_kwargs = mock_request.call_args + url = call_kwargs.kwargs.get("url", "") or ( + call_kwargs.args[1] if len(call_kwargs.args) > 1 else "" + ) + headers = call_kwargs.kwargs.get("headers", {}) + assert "internal-api/resources" in url + assert "X-Internal-Api-Key" in headers + + get_entitlements_backend.cache_clear() + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, + CALDAV_INTERNAL_API_KEY="test-internal-key", +) +def test_delete_resource(): + """DELETE /resources/{resource_id}/ deletes the resource via internal API.""" + get_entitlements_backend.cache_clear() + org = factories.OrganizationFactory(external_id="test-org") + admin = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=admin) + + resource_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + + with mock.patch("core.services.caldav_service.requests.request") as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"deleted": True} + mock_response.text = '{"deleted": true}' + mock_request.return_value = mock_response + + response = client.delete(f"/api/v1.0/resources/{resource_id}/") + + assert response.status_code == HTTP_204_NO_CONTENT + + # Verify the HTTP call went to the internal API + mock_request.assert_called_once() + call_kwargs = mock_request.call_args + url = call_kwargs.kwargs.get("url", "") or ( + call_kwargs.args[1] if len(call_kwargs.args) > 1 else "" + ) + headers = call_kwargs.kwargs.get("headers", {}) + assert f"internal-api/resources/{resource_id}" in url + assert "X-Internal-Api-Key" in headers + + get_entitlements_backend.cache_clear() + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, + CALDAV_INTERNAL_API_KEY="test-internal-key", +) +def test_delete_resource_cross_org_blocked(): + """Cannot delete a resource from another organization.""" + get_entitlements_backend.cache_clear() + org_a = factories.OrganizationFactory(external_id="org-a") + admin = factories.UserFactory(organization=org_a) + + client = APIClient() + client.force_authenticate(user=admin) + + with mock.patch("core.services.caldav_service.requests.request") as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 403 + mock_response.json.return_value = { + "error": "Cannot delete a resource from a different organization." + } + mock_response.text = ( + '{"error": "Cannot delete a resource from a different organization."}' + ) + mock_request.return_value = mock_response + + response = client.delete( + "/api/v1.0/resources/b1b2c3d4-e5f6-7890-abcd-ef1234567890/" + ) + + assert response.status_code == HTTP_400_BAD_REQUEST + assert "different organization" in response.json()["detail"] + + get_entitlements_backend.cache_clear() + + +# -- Lateral access tests -- + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, + CALDAV_INTERNAL_API_KEY="test-internal-key", +) +def test_create_resource_sends_user_org_id(): + """Create resource always sends the authenticated user's org_id, not a caller-supplied one.""" + get_entitlements_backend.cache_clear() + org = factories.OrganizationFactory(external_id="org-alpha") + admin = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=admin) + + with mock.patch("core.services.caldav_service.requests.request") as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 201 + mock_response.text = '{"principal_uri": "principals/resources/x"}' + mock_request.return_value = mock_response + + response = client.post( + "/api/v1.0/resources/", + {"name": "Room 1"}, + format="json", + ) + + assert response.status_code == HTTP_201_CREATED + # Verify the JSON body sent to internal API contains the user's org + call_kwargs = mock_request.call_args + body = json.loads(call_kwargs.kwargs.get("data", b"{}")) + assert body["org_id"] == str(org.id) + + get_entitlements_backend.cache_clear() + + +@pytest.mark.django_db +@override_settings( + ENTITLEMENTS_BACKEND="core.entitlements.backends.local.LocalEntitlementsBackend", + ENTITLEMENTS_BACKEND_PARAMETERS={}, + CALDAV_INTERNAL_API_KEY="test-internal-key", +) +def test_delete_resource_sends_user_org_id(): + """Delete resource sends the authenticated user's org_id in the header.""" + get_entitlements_backend.cache_clear() + org = factories.OrganizationFactory(external_id="org-beta") + admin = factories.UserFactory(organization=org) + + client = APIClient() + client.force_authenticate(user=admin) + + resource_id = "a1b2c3d4-0000-0000-0000-000000000001" + + with mock.patch("core.services.caldav_service.requests.request") as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.text = '{"deleted": true}' + mock_request.return_value = mock_response + + response = client.delete(f"/api/v1.0/resources/{resource_id}/") + + assert response.status_code == HTTP_204_NO_CONTENT + call_kwargs = mock_request.call_args + headers = call_kwargs.kwargs.get("headers", {}) + assert headers.get("X-CalDAV-Organization") == str(org.id) + + get_entitlements_backend.cache_clear() + + +# -- Path traversal tests -- + + +class TestResourceIdValidation: + """Tests that resource_id is validated as a UUID to prevent path traversal.""" + + @pytest.fixture(autouse=True) + def _internal_api_key(self, settings): + settings.CALDAV_INTERNAL_API_KEY = "test-internal-key" + + def test_delete_rejects_path_traversal(self): + """A malicious resource_id like ../../users/victim is rejected.""" + user = mock.Mock() + user.organization_id = "some-org" + service = ResourceService() + + with pytest.raises(ResourceProvisioningError, match="Invalid resource ID"): + service.delete_resource(user, "../../users/victim@example.com") + + def test_delete_rejects_non_uuid_string(self): + """A non-UUID resource_id is rejected.""" + user = mock.Mock() + user.organization_id = "some-org" + service = ResourceService() + + with pytest.raises(ResourceProvisioningError, match="Invalid resource ID"): + service.delete_resource(user, "not-a-uuid") + + def test_delete_accepts_valid_uuid(self): + """A valid UUID resource_id passes validation.""" + user = mock.Mock() + user.email = "admin@example.com" + user.organization_id = "some-org" + service = ResourceService() + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_request.return_value = mock_response + + # Should not raise + service.delete_resource(user, "a1b2c3d4-e5f6-7890-abcd-ef1234567890") + + def test_create_resource_rejects_missing_api_key(self, settings): + """create_resource raises when CALDAV_INTERNAL_API_KEY is empty.""" + settings.CALDAV_INTERNAL_API_KEY = "" + user = mock.Mock() + user.organization_id = "some-org" + service = ResourceService() + + with pytest.raises(ResourceProvisioningError, match="CALDAV_INTERNAL_API_KEY"): + service.create_resource(user, "Room 1", "ROOM") + + def test_delete_resource_rejects_missing_api_key(self, settings): + """delete_resource raises when CALDAV_INTERNAL_API_KEY is empty.""" + settings.CALDAV_INTERNAL_API_KEY = "" + user = mock.Mock() + user.organization_id = "some-org" + service = ResourceService() + + with pytest.raises(ResourceProvisioningError, match="CALDAV_INTERNAL_API_KEY"): + service.delete_resource(user, "a1b2c3d4-e5f6-7890-abcd-ef1234567890") diff --git a/src/backend/core/tests/test_rsvp.py b/src/backend/core/tests/test_rsvp.py index d5598e3..5bcaf7e 100644 --- a/src/backend/core/tests/test_rsvp.py +++ b/src/backend/core/tests/test_rsvp.py @@ -8,7 +8,7 @@ from unittest.mock import patch from urllib.parse import parse_qs, urlparse from django.core import mail -from django.core.signing import BadSignature, Signer +from django.core.signing import BadSignature, SignatureExpired, TimestampSigner from django.template.loader import render_to_string from django.test import RequestFactory, TestCase, override_settings from django.utils import timezone @@ -16,7 +16,8 @@ from django.utils import timezone import icalendar import pytest -from core.api.viewsets_rsvp import RSVPView +from core import factories +from core.api.viewsets_rsvp import RSVPConfirmView, RSVPProcessView from core.services.caldav_service import CalDAVHTTPClient from core.services.calendar_invitation_service import ( CalendarInvitationService, @@ -56,10 +57,10 @@ SAMPLE_CALDAV_RESPONSE = """\ - /api/v1.0/caldav/calendars/alice%40example.com/cal-uuid/test-uid-123.ics + /caldav/calendars/users/alice%40example.com/cal-uuid/test-uid-123.ics - /api/v1.0/caldav/calendars/alice%40example.com/cal-uuid/test-uid-123.ics + /caldav/calendars/users/alice%40example.com/cal-uuid/test-uid-123.ics {ics_data} HTTP/1.1 200 OK @@ -71,8 +72,8 @@ SAMPLE_CALDAV_RESPONSE = """\ def _make_token( uid="test-uid-123", email="bob@example.com", organizer="alice@example.com" ): - """Create a valid signed RSVP token.""" - signer = Signer(salt="rsvp") + """Create a valid signed RSVP token using TimestampSigner.""" + signer = TimestampSigner(salt="rsvp") return signer.sign_object( { "uid": uid, @@ -88,7 +89,7 @@ class TestRSVPTokenGeneration: def test_token_roundtrip(self): """A generated token can be unsigned to recover the payload.""" token = _make_token() - signer = Signer(salt="rsvp") + signer = TimestampSigner(salt="rsvp") payload = signer.unsign_object(token) assert payload["uid"] == "test-uid-123" assert payload["email"] == "bob@example.com" @@ -97,7 +98,7 @@ class TestRSVPTokenGeneration: def test_tampered_token_fails(self): """A tampered token raises BadSignature.""" token = _make_token() + "tampered" - signer = Signer(salt="rsvp") + signer = TimestampSigner(salt="rsvp") with pytest.raises(BadSignature): signer.unsign_object(token) @@ -194,7 +195,7 @@ class TestRSVPEmailTemplateRendering: class TestUpdateAttendeePartstat: - """Tests for the _update_attendee_partstat function.""" + """Tests for the update_attendee_partstat function.""" def test_update_existing_partstat(self): result = CalDAVHTTPClient.update_attendee_partstat( @@ -232,18 +233,50 @@ class TestUpdateAttendeePartstat: assert "CN=Bob" in result assert "mailto:bob@example.com" in result + def test_substring_email_does_not_match(self): + """Emails that are substrings of the target should NOT match.""" + # Create ICS with a similar-but-different email + ics = ( + "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//Test//EN\r\n" + "BEGIN:VEVENT\r\n" + "UID:test-substring\r\n" + "DTSTART:20260401T100000Z\r\n" + "DTEND:20260401T110000Z\r\n" + "SUMMARY:Test\r\n" + "ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:notbob@example.com\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR" + ) + # "bob@example.com" should NOT match "notbob@example.com" + result = CalDAVHTTPClient.update_attendee_partstat( + ics, "bob@example.com", "ACCEPTED" + ) + assert result is None + @override_settings( CALDAV_URL="http://caldav:80", CALDAV_OUTBOUND_API_KEY="test-api-key", - APP_URL="http://localhost:8921", + APP_URL="http://localhost:8931", + API_VERSION="v1.0", ) -class TestRSVPView(TestCase): - """Tests for the RSVPView.""" +class TestRSVPConfirmView(TestCase): + """Tests for the RSVPConfirmView (GET handler).""" def setUp(self): self.factory = RequestFactory() - self.view = RSVPView.as_view() + self.view = RSVPConfirmView.as_view() + + def test_valid_token_renders_confirm_page(self): + token = _make_token() + request = self.factory.get("/rsvp/", {"token": token, "action": "accepted"}) + response = self.view(request) + assert response.status_code == 200 + content = response.content.decode() + assert 'method="post"' in content + assert token in content def test_invalid_action_returns_400(self): token = _make_token() @@ -251,12 +284,6 @@ class TestRSVPView(TestCase): response = self.view(request) assert response.status_code == 400 - def test_missing_action_returns_400(self): - token = _make_token() - request = self.factory.get("/rsvp/", {"token": token}) - response = self.view(request) - assert response.status_code == 400 - def test_invalid_token_returns_400(self): request = self.factory.get( "/rsvp/", {"token": "bad-token", "action": "accepted"} @@ -264,8 +291,46 @@ class TestRSVPView(TestCase): response = self.view(request) assert response.status_code == 400 + +@override_settings( + CALDAV_URL="http://caldav:80", + CALDAV_OUTBOUND_API_KEY="test-api-key", + APP_URL="http://localhost:8931", + API_VERSION="v1.0", +) +class TestRSVPProcessView(TestCase): + """Tests for the RSVPProcessView (POST handler).""" + + def setUp(self): + self.factory = RequestFactory() + self.view = RSVPProcessView.as_view() + # RSVP view looks up organizer from DB + self.organizer = factories.UserFactory(email="alice@example.com") + + def _post(self, token, action): + request = self.factory.post( + "/api/v1.0/rsvp/", + {"token": token, "action": action}, + ) + return self.view(request) + + def test_invalid_action_returns_400(self): + token = _make_token() + response = self._post(token, "invalid") + assert response.status_code == 400 + + def test_missing_action_returns_400(self): + token = _make_token() + request = self.factory.post("/api/v1.0/rsvp/", {"token": token}) + response = self.view(request) + assert response.status_code == 400 + + def test_invalid_token_returns_400(self): + response = self._post("bad-token", "accepted") + assert response.status_code == 400 + def test_missing_token_returns_400(self): - request = self.factory.get("/rsvp/", {"action": "accepted"}) + request = self.factory.post("/api/v1.0/rsvp/", {"action": "accepted"}) response = self.view(request) assert response.status_code == 400 @@ -275,33 +340,37 @@ class TestRSVPView(TestCase): """Full accept flow: find event, update partstat, put back.""" mock_find.return_value = ( SAMPLE_ICS, - "/api/v1.0/caldav/calendars/alice%40example.com/cal/event.ics", + "/caldav/calendars/users/alice%40example.com/cal/event.ics", + '"etag-123"', ) mock_put.return_value = True token = _make_token() - request = self.factory.get("/rsvp/", {"token": token, "action": "accepted"}) - response = self.view(request) + response = self._post(token, "accepted") assert response.status_code == 200 assert "accepted the invitation" in response.content.decode() # Verify CalDAV calls - mock_find.assert_called_once_with("alice@example.com", "test-uid-123") + mock_find.assert_called_once() + find_args = mock_find.call_args[0] + assert find_args[0].email == "alice@example.com" + assert find_args[1] == "test-uid-123" mock_put.assert_called_once() # Check the updated data contains ACCEPTED put_args = mock_put.call_args assert "PARTSTAT=ACCEPTED" in put_args[0][2] + # Check ETag is passed + assert put_args[1]["etag"] == '"etag-123"' @patch.object(CalDAVHTTPClient, "put_event") @patch.object(CalDAVHTTPClient, "find_event_by_uid") def test_decline_flow(self, mock_find, mock_put): - mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics") + mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics", None) mock_put.return_value = True token = _make_token() - request = self.factory.get("/rsvp/", {"token": token, "action": "declined"}) - response = self.view(request) + response = self._post(token, "declined") assert response.status_code == 200 assert "declined the invitation" in response.content.decode() @@ -311,12 +380,11 @@ class TestRSVPView(TestCase): @patch.object(CalDAVHTTPClient, "put_event") @patch.object(CalDAVHTTPClient, "find_event_by_uid") def test_tentative_flow(self, mock_find, mock_put): - mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics") + mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics", None) mock_put.return_value = True token = _make_token() - request = self.factory.get("/rsvp/", {"token": token, "action": "tentative"}) - response = self.view(request) + response = self._post(token, "tentative") assert response.status_code == 200 content = response.content.decode() @@ -326,11 +394,10 @@ class TestRSVPView(TestCase): @patch.object(CalDAVHTTPClient, "find_event_by_uid") def test_event_not_found_returns_400(self, mock_find): - mock_find.return_value = (None, None) + mock_find.return_value = (None, None, None) token = _make_token() - request = self.factory.get("/rsvp/", {"token": token, "action": "accepted"}) - response = self.view(request) + response = self._post(token, "accepted") assert response.status_code == 400 assert "not found" in response.content.decode().lower() @@ -338,12 +405,11 @@ class TestRSVPView(TestCase): @patch.object(CalDAVHTTPClient, "put_event") @patch.object(CalDAVHTTPClient, "find_event_by_uid") def test_put_failure_returns_400(self, mock_find, mock_put): - mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics") + mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics", None) mock_put.return_value = False token = _make_token() - request = self.factory.get("/rsvp/", {"token": token, "action": "accepted"}) - response = self.view(request) + response = self._post(token, "accepted") assert response.status_code == 400 assert "error occurred" in response.content.decode().lower() @@ -351,12 +417,11 @@ class TestRSVPView(TestCase): @patch.object(CalDAVHTTPClient, "find_event_by_uid") def test_attendee_not_in_event_returns_400(self, mock_find): """If the attendee email is not in the event, return error.""" - mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics") + mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics", None) # Token with an email that's not in the event token = _make_token(email="stranger@example.com") - request = self.factory.get("/rsvp/", {"token": token, "action": "accepted"}) - response = self.view(request) + response = self._post(token, "accepted") assert response.status_code == 400 assert "not listed" in response.content.decode().lower() @@ -364,11 +429,10 @@ class TestRSVPView(TestCase): @patch.object(CalDAVHTTPClient, "find_event_by_uid") def test_past_event_returns_400(self, mock_find): """Cannot RSVP to an event that has already ended.""" - mock_find.return_value = (SAMPLE_ICS_PAST, "/path/to/event.ics") + mock_find.return_value = (SAMPLE_ICS_PAST, "/path/to/event.ics", None) token = _make_token(uid="test-uid-past") - request = self.factory.get("/rsvp/", {"token": token, "action": "accepted"}) - response = self.view(request) + response = self._post(token, "accepted") assert response.status_code == 400 assert "already passed" in response.content.decode().lower() @@ -430,28 +494,29 @@ class TestItipSetting: CALDAV_URL="http://caldav:80", CALDAV_OUTBOUND_API_KEY="test-api-key", CALDAV_INBOUND_API_KEY="test-inbound-key", - APP_URL="http://localhost:8921", + APP_URL="http://localhost:8931", + API_VERSION="v1.0", EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", ) class TestRSVPEndToEndFlow(TestCase): """ - Integration test: scheduling callback sends email → extract RSVP links - → follow link → verify event is updated. - - This tests the full flow from CalDAV scheduling callback to RSVP response, - using Django's in-memory email backend to intercept sent emails. + Integration test: scheduling callback sends email -> extract RSVP links + -> follow link (GET confirm -> POST process) -> verify event is updated. """ def setUp(self): self.factory = RequestFactory() - self.rsvp_view = RSVPView.as_view() + self.confirm_view = RSVPConfirmView.as_view() + self.process_view = RSVPProcessView.as_view() + self.organizer = factories.UserFactory(email="alice@example.com") def test_email_to_rsvp_accept_flow(self): """ 1. CalDAV scheduling callback sends an invitation email 2. Extract RSVP accept link from the email HTML - 3. Follow the RSVP link - 4. Verify the event PARTSTAT is updated to ACCEPTED + 3. GET the RSVP link (renders auto-submit form) + 4. POST to process the RSVP + 5. Verify the event PARTSTAT is updated to ACCEPTED """ # Step 1: Send invitation via the CalendarInvitationService service = CalendarInvitationService() @@ -488,22 +553,32 @@ class TestRSVPEndToEndFlow(TestCase): assert "token" in params assert params["action"] == ["accepted"] - # Step 4: Follow the RSVP link (mock CalDAV interactions) + # Step 3b: GET the confirm page + request = self.factory.get( + "/rsvp/", + {"token": params["token"][0], "action": "accepted"}, + ) + response = self.confirm_view(request) + assert response.status_code == 200 + assert 'method="post"' in response.content.decode() + + # Step 4: POST to process the RSVP (mock CalDAV interactions) with ( patch.object(CalDAVHTTPClient, "find_event_by_uid") as mock_find, patch.object(CalDAVHTTPClient, "put_event") as mock_put, ): mock_find.return_value = ( SAMPLE_ICS, - "/api/v1.0/caldav/calendars/alice%40example.com/cal/event.ics", + "/caldav/calendars/users/alice%40example.com/cal/event.ics", + '"etag-abc"', ) mock_put.return_value = True - request = self.factory.get( - "/rsvp/", + request = self.factory.post( + "/api/v1.0/rsvp/", {"token": params["token"][0], "action": "accepted"}, ) - response = self.rsvp_view(request) + response = self.process_view(request) # Step 5: Verify success assert response.status_code == 200 @@ -511,7 +586,10 @@ class TestRSVPEndToEndFlow(TestCase): assert "accepted the invitation" in content # Verify CalDAV was called with the right data - mock_find.assert_called_once_with("alice@example.com", "test-uid-123") + mock_find.assert_called_once() + find_args = mock_find.call_args[0] + assert find_args[0].email == "alice@example.com" + assert find_args[1] == "test-uid-123" mock_put.assert_called_once() put_data = mock_put.call_args[0][2] assert "PARTSTAT=ACCEPTED" in put_data @@ -542,14 +620,14 @@ class TestRSVPEndToEndFlow(TestCase): patch.object(CalDAVHTTPClient, "find_event_by_uid") as mock_find, patch.object(CalDAVHTTPClient, "put_event") as mock_put, ): - mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics") + mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics", None) mock_put.return_value = True - request = self.factory.get( - "/rsvp/", + request = self.factory.post( + "/api/v1.0/rsvp/", {"token": params["token"][0], "action": "declined"}, ) - response = self.rsvp_view(request) + response = self.process_view(request) assert response.status_code == 200 assert "declined the invitation" in response.content.decode() @@ -612,13 +690,134 @@ class TestRSVPEndToEndFlow(TestCase): params = parse_qs(parsed.query) # The event is in the past - mock_find.return_value = (SAMPLE_ICS_PAST, "/path/to/event.ics") + mock_find.return_value = (SAMPLE_ICS_PAST, "/path/to/event.ics", None) - request = self.factory.get( - "/rsvp/", + request = self.factory.post( + "/api/v1.0/rsvp/", {"token": params["token"][0], "action": "accepted"}, ) - response = self.rsvp_view(request) + response = self.process_view(request) assert response.status_code == 400 assert "already passed" in response.content.decode().lower() + + +def _make_recurring_ics( + uid="recurring-uid-1", summary="Weekly Standup", days_from_now=30 +): + """Build a recurring ICS string with an RRULE.""" + dt = timezone.now() + timedelta(days=days_from_now) + dtstart = dt.strftime("%Y%m%dT%H%M%SZ") + dtend = (dt + timedelta(hours=1)).strftime("%Y%m%dT%H%M%SZ") + return ( + "BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + "PRODID:-//Test//EN\r\n" + "BEGIN:VEVENT\r\n" + f"UID:{uid}\r\n" + f"DTSTART:{dtstart}\r\n" + f"DTEND:{dtend}\r\n" + f"SUMMARY:{summary}\r\n" + "RRULE:FREQ=WEEKLY;COUNT=52\r\n" + "ORGANIZER;CN=Alice:mailto:alice@example.com\r\n" + "ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:bob@example.com\r\n" + "SEQUENCE:0\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR" + ) + + +@override_settings( + CALDAV_URL="http://caldav:80", + CALDAV_OUTBOUND_API_KEY="test-api-key", + APP_URL="http://localhost:8931", + API_VERSION="v1.0", + RSVP_TOKEN_MAX_AGE_RECURRING=7776000, # 90 days +) +class TestRSVPRecurringTokenExpiry(TestCase): + """Tests for RSVP token max_age enforcement on recurring events. + + Tokens are signed with TimestampSigner so that the max_age check + works correctly for recurring events. + """ + + def setUp(self): + self.factory = RequestFactory() + self.view = RSVPProcessView.as_view() + self.organizer = factories.UserFactory(email="alice@example.com") + + def _post(self, token, action): + request = self.factory.post( + "/api/v1.0/rsvp/", + {"token": token, "action": action}, + ) + return self.view(request) + + @patch.object(CalDAVHTTPClient, "put_event") + @patch.object(CalDAVHTTPClient, "find_event_by_uid") + def test_recurring_event_with_fresh_token_succeeds(self, mock_find, mock_put): + """A fresh token for a recurring event should be accepted.""" + ics = _make_recurring_ics() + mock_find.return_value = (ics, "/path/to/event.ics", None) + mock_put.return_value = True + + token = _make_token(uid="recurring-uid-1") + response = self._post(token, "accepted") + + assert response.status_code == 200 + + @patch.object(CalDAVHTTPClient, "find_event_by_uid") + def test_recurring_event_with_expired_token_rejected(self, mock_find): + """An expired token for a recurring event should be rejected.""" + ics = _make_recurring_ics() + mock_find.return_value = (ics, "/path/to/event.ics", None) + + token = _make_token(uid="recurring-uid-1") + + # Simulate time passing beyond max_age by patching unsign_object + # to raise SignatureExpired on the second call (with max_age) + original_unsign = TimestampSigner.unsign_object + + def side_effect(value, **kwargs): + if kwargs.get("max_age") is not None: + raise SignatureExpired("Signature age exceeds max_age") + return original_unsign(TimestampSigner(), value, **kwargs) + + with patch.object(TimestampSigner, "unsign_object", side_effect=side_effect): + response = self._post(token, "accepted") + + assert response.status_code == 400 + content = response.content.decode().lower() + assert "expired" in content or "new invitation" in content + + @patch.object(CalDAVHTTPClient, "find_event_by_uid") + def test_recurring_event_token_expired_via_freeze_time(self, mock_find): + """Token created now should be rejected after max_age seconds.""" + from freezegun import ( # noqa: PLC0415 # pylint: disable=import-outside-toplevel + freeze_time, + ) + + ics = _make_recurring_ics() + mock_find.return_value = (ics, "/path/to/event.ics", None) + + token = _make_token(uid="recurring-uid-1") + + # Advance time beyond RSVP_TOKEN_MAX_AGE_RECURRING (90 days = 7776000s) + with freeze_time(timezone.now() + timedelta(days=91)): + response = self._post(token, "accepted") + + assert response.status_code == 400 + content = response.content.decode().lower() + assert "expired" in content or "new invitation" in content + + @patch.object(CalDAVHTTPClient, "put_event") + @patch.object(CalDAVHTTPClient, "find_event_by_uid") + def test_non_recurring_event_ignores_token_age(self, mock_find, mock_put): + """Non-recurring events should not enforce token max_age.""" + mock_find.return_value = (SAMPLE_ICS, "/path/to/event.ics", None) + mock_put.return_value = True + + token = _make_token() + response = self._post(token, "accepted") + + assert response.status_code == 200 diff --git a/src/backend/core/tests/test_signals.py b/src/backend/core/tests/test_signals.py new file mode 100644 index 0000000..d43530a --- /dev/null +++ b/src/backend/core/tests/test_signals.py @@ -0,0 +1,205 @@ +"""Tests for Django signals (CalDAV cleanup on user/org deletion).""" + +import json +from unittest import mock + +from django.test import TestCase, override_settings + +import pytest + +from core import factories +from core.models import Organization, User + +# Signal tests need real signal handlers (they mock at the requests.request +# level), so they must be in the caldav xdist group to skip the conftest +# fixture that disconnects signals for non-CalDAV tests. +pytestmark = pytest.mark.xdist_group("caldav") + + +@override_settings( + CALDAV_URL="http://caldav:80", + CALDAV_INTERNAL_API_KEY="test-internal-key", + CALDAV_OUTBOUND_API_KEY="test-api-key", +) +class TestDeleteUserCaldavData(TestCase): + """Tests for the delete_user_caldav_data pre_delete signal.""" + + def test_deleting_user_calls_internal_api(self): + """Deleting a user triggers a POST to the SabreDAV internal API.""" + user = factories.UserFactory(email="alice@example.com") + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_request.return_value = mock_response + + with self.captureOnCommitCallbacks(execute=True): + user.delete() + + # Verify the internal API was called to clean up CalDAV data + mock_request.assert_called_once() + call_kwargs = mock_request.call_args + assert call_kwargs.kwargs["method"] == "POST" + url = call_kwargs.kwargs.get("url", "") + assert "internal-api/users/delete" in url + body = json.loads(call_kwargs.kwargs.get("data", b"{}")) + assert body["email"] == "alice@example.com" + headers = call_kwargs.kwargs.get("headers", {}) + assert headers.get("X-Internal-Api-Key") == "test-internal-key" + + def test_deleting_user_without_email_skips_cleanup(self): + """Users without an email don't trigger CalDAV cleanup.""" + user = factories.UserFactory(email="") + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + user.delete() + + mock_request.assert_not_called() + + @override_settings(CALDAV_INTERNAL_API_KEY="") + def test_deleting_user_without_api_key_skips_cleanup(self): + """When CALDAV_INTERNAL_API_KEY is empty, cleanup is skipped.""" + user = factories.UserFactory(email="alice@example.com") + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + user.delete() + + mock_request.assert_not_called() + + def test_deleting_user_handles_http_error_gracefully(self): + """HTTP errors during cleanup don't prevent user deletion.""" + user = factories.UserFactory(email="alice@example.com") + + with mock.patch( + "core.services.caldav_service.requests.request", + side_effect=Exception("Connection refused"), + ): + # Should not raise — the signal catches exceptions + with self.captureOnCommitCallbacks(execute=True): + user.delete() + + assert not User.objects.filter(email="alice@example.com").exists() + + +@override_settings( + CALDAV_URL="http://caldav:80", + CALDAV_INTERNAL_API_KEY="test-internal-key", + CALDAV_OUTBOUND_API_KEY="test-api-key", +) +class TestDeleteOrganizationCaldavData(TestCase): + """Tests for the delete_organization_caldav_data pre_delete signal.""" + + def test_deleting_org_cleans_up_all_members(self): + """Deleting an org triggers CalDAV cleanup for every member. + + cleanup_organization_caldav_data calls DELETE for each member, + then members.delete() triggers the user pre_delete signal which + schedules on_commit callbacks. So we expect 2 calls from org + cleanup + 2 from user signals = 4 total. + """ + org = factories.OrganizationFactory(external_id="doomed-org") + factories.UserFactory(email="alice@example.com", organization=org) + factories.UserFactory(email="bob@example.com", organization=org) + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_request.return_value = mock_response + + with self.captureOnCommitCallbacks(execute=True): + org.delete() + + # 2 members x 2 POST calls each (org cleanup + user signal on_commit) + assert mock_request.call_count == 4 + bodies = [ + json.loads(call.kwargs.get("data", b"{}")) + for call in mock_request.call_args_list + ] + emails = [b.get("email", "") for b in bodies] + assert "alice@example.com" in emails + assert "bob@example.com" in emails + + def test_deleting_org_deletes_member_users(self): + """Deleting an org also deletes member Django User objects.""" + org = factories.OrganizationFactory(external_id="doomed-org") + factories.UserFactory(email="alice@example.com", organization=org) + factories.UserFactory(email="bob@example.com", organization=org) + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_request.return_value = mock_response + + with self.captureOnCommitCallbacks(execute=True): + org.delete() + + assert not User.objects.filter(email="alice@example.com").exists() + assert not User.objects.filter(email="bob@example.com").exists() + assert not Organization.objects.filter(external_id="doomed-org").exists() + + def test_deleting_org_with_no_members(self): + """Deleting an org with no members succeeds without errors.""" + org = factories.OrganizationFactory(external_id="empty-org") + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + org.delete() + + mock_request.assert_not_called() + + def test_deleting_org_continues_after_member_cleanup_failure(self): + """If CalDAV cleanup fails for one member, other members still cleaned up.""" + org = factories.OrganizationFactory(external_id="doomed-org") + factories.UserFactory(email="alice@example.com", organization=org) + factories.UserFactory(email="bob@example.com", organization=org) + + call_count = 0 + + def side_effect(**kwargs): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise Exception("Network error") # pylint: disable=broad-exception-raised + resp = mock.Mock() + resp.status_code = 200 + return resp + + with mock.patch( + "core.services.caldav_service.requests.request", + side_effect=side_effect, + ): + with self.captureOnCommitCallbacks(execute=True): + org.delete() + + # Org cleanup: 2 calls (1 fails, 1 succeeds), then user signal: 2 more + assert call_count == 4 + assert not Organization.objects.filter(external_id="doomed-org").exists() + + @override_settings(CALDAV_INTERNAL_API_KEY="") + def test_deleting_org_without_api_key_skips_caldav_cleanup(self): + """When CALDAV_INTERNAL_API_KEY is empty, CalDAV cleanup is skipped.""" + org = factories.OrganizationFactory(external_id="org-nokey") + factories.UserFactory(email="alice@example.com", organization=org) + + with mock.patch( + "core.services.caldav_service.requests.request" + ) as mock_request: + # Without the API key, the signal skips CalDAV cleanup but + # also doesn't delete members, so PROTECT FK blocks deletion. + try: + org.delete() + except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught + pass + + mock_request.assert_not_called() diff --git a/src/backend/core/tests/test_tasks.py b/src/backend/core/tests/test_tasks.py new file mode 100644 index 0000000..de44069 --- /dev/null +++ b/src/backend/core/tests/test_tasks.py @@ -0,0 +1,422 @@ +"""Tests for the task system: task functions, polling endpoint, and async dispatch.""" + +# pylint: disable=import-outside-toplevel + +import uuid +from unittest.mock import MagicMock, patch + +from django.core.cache import cache + +import pytest +from rest_framework.test import APIClient + +from core import factories +from core.services.import_service import ImportResult + +pytestmark = pytest.mark.django_db + + +# --------------------------------------------------------------------------- +# Test import_events_task function directly +# --------------------------------------------------------------------------- + + +class TestImportEventsTask: + """Test the import_events_task function itself (via EagerBroker).""" + + @patch("core.tasks.ICSImportService") + def test_task_returns_success_result(self, mock_service_cls): + """Task should return a SUCCESS dict with import results.""" + from core.tasks import import_events_task # noqa: PLC0415 + + mock_service = mock_service_cls.return_value + mock_service.import_events.return_value = ImportResult( + total_events=3, + imported_count=3, + duplicate_count=0, + skipped_count=0, + errors=[], + ) + + user = factories.UserFactory() + ics_data = b"BEGIN:VCALENDAR\r\nEND:VCALENDAR" + caldav_path = f"/calendars/users/{user.email}/{uuid.uuid4()}/" + + result = import_events_task(str(user.id), caldav_path, ics_data.hex()) + + assert result["status"] == "SUCCESS" + assert result["result"]["total_events"] == 3 + assert result["result"]["imported_count"] == 3 + assert result["error"] is None + mock_service.import_events.assert_called_once_with(user, caldav_path, ics_data) + + @patch("core.tasks.ICSImportService") + def test_task_user_not_found(self, mock_service_cls): + """Task should return FAILURE if user does not exist.""" + from core.tasks import import_events_task # noqa: PLC0415 + + result = import_events_task( + str(uuid.uuid4()), # non-existent user + "/calendars/users/nobody@example.com/cal/", + b"dummy".hex(), + ) + + assert result["status"] == "FAILURE" + assert "User not found" in result["error"] + mock_service_cls.return_value.import_events.assert_not_called() + + @patch("core.tasks.set_task_progress") + @patch("core.tasks.ICSImportService") + def test_task_reports_progress(self, mock_service_cls, mock_progress): + """Task should call set_task_progress at 0%, 10%, and 100%.""" + from core.tasks import import_events_task # noqa: PLC0415 + + mock_service_cls.return_value.import_events.return_value = ImportResult( + total_events=1, + imported_count=1, + duplicate_count=0, + skipped_count=0, + errors=[], + ) + + user = factories.UserFactory() + import_events_task( + str(user.id), + f"/calendars/users/{user.email}/{uuid.uuid4()}/", + b"data".hex(), + ) + + progress_values = [call.args[0] for call in mock_progress.call_args_list] + assert progress_values == [0, 10, 100] + + @patch("core.tasks.ICSImportService") + def test_task_via_delay(self, mock_service_cls): + """Calling .delay() should dispatch and return a Task with an id.""" + from core.tasks import import_events_task # noqa: PLC0415 + + mock_service_cls.return_value.import_events.return_value = ImportResult( + total_events=1, + imported_count=1, + duplicate_count=0, + skipped_count=0, + errors=[], + ) + + user = factories.UserFactory() + task = import_events_task.delay( + str(user.id), + f"/calendars/users/{user.email}/{uuid.uuid4()}/", + b"data".hex(), + ) + + assert task.id is not None + assert isinstance(task.id, str) + + +# --------------------------------------------------------------------------- +# Test TaskDetailView polling endpoint +# --------------------------------------------------------------------------- + + +class TestTaskDetailView: + """Test the /api/v1.0/tasks// polling endpoint.""" + + TASK_URL = "/api/v1.0/tasks/{task_id}/" + + def test_requires_authentication(self): + """Unauthenticated requests should be rejected.""" + client = APIClient() + response = client.get(self.TASK_URL.format(task_id="some-id")) + assert response.status_code == 401 + + def test_task_not_found(self): + """Unknown task_id should return 404.""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + response = client.get(self.TASK_URL.format(task_id=str(uuid.uuid4()))) + assert response.status_code == 404 + assert response.json()["status"] == "FAILURE" + + def test_task_forbidden_for_other_user(self): + """Users cannot poll tasks they don't own.""" + owner = factories.UserFactory() + other = factories.UserFactory() + + # Simulate a tracked task owned by `owner` + task_id = str(uuid.uuid4()) + import json # noqa: PLC0415 + + cache.set( + f"task_tracking:{task_id}", + json.dumps( + { + "owner": str(owner.id), + "actor_name": "import_events_task", + "queue_name": "import", + } + ), + ) + + client = APIClient() + client.force_login(other) + + response = client.get(self.TASK_URL.format(task_id=task_id)) + assert response.status_code == 403 + + @patch("core.tasks.ICSImportService") + def test_poll_completed_task(self, mock_service_cls): + """Polling a completed task should return SUCCESS with results.""" + from core.tasks import import_events_task # noqa: PLC0415 + + expected_result = ImportResult( + total_events=5, + imported_count=4, + duplicate_count=1, + skipped_count=0, + errors=[], + ) + mock_service_cls.return_value.import_events.return_value = expected_result + + user = factories.UserFactory() + caldav_path = f"/calendars/users/{user.email}/{uuid.uuid4()}/" + + # Dispatch via .delay() — EagerBroker runs it synchronously + task = import_events_task.delay(str(user.id), caldav_path, b"data".hex()) + task.track_owner(user.id) + + client = APIClient() + client.force_login(user) + + response = client.get(self.TASK_URL.format(task_id=task.id)) + assert response.status_code == 200 + + data = response.json() + assert data["status"] == "SUCCESS" + assert data["result"]["total_events"] == 5 + assert data["result"]["imported_count"] == 4 + assert data["error"] is None + + @patch("core.tasks.ICSImportService") + def test_poll_task_owner_matches(self, mock_service_cls): + """Only the task owner can poll the task.""" + from core.tasks import import_events_task # noqa: PLC0415 + + mock_service_cls.return_value.import_events.return_value = ImportResult( + total_events=1, + imported_count=1, + duplicate_count=0, + skipped_count=0, + errors=[], + ) + + owner = factories.UserFactory() + other = factories.UserFactory() + caldav_path = f"/calendars/users/{owner.email}/{uuid.uuid4()}/" + + task = import_events_task.delay(str(owner.id), caldav_path, b"data".hex()) + task.track_owner(owner.id) + + client = APIClient() + + # Other user gets 403 + client.force_login(other) + response = client.get(self.TASK_URL.format(task_id=task.id)) + assert response.status_code == 403 + + # Owner gets 200 + client.force_login(owner) + response = client.get(self.TASK_URL.format(task_id=task.id)) + assert response.status_code == 200 + assert response.json()["status"] == "SUCCESS" + + +# --------------------------------------------------------------------------- +# Test API → task dispatch integration +# --------------------------------------------------------------------------- + + +class TestImportAPITaskDispatch: + """Test that the import API correctly dispatches a task.""" + + IMPORT_URL = "/api/v1.0/calendars/import-events/" + + @patch("core.tasks.import_events_task") + def test_api_calls_delay_with_correct_args(self, mock_task): + """The API should call .delay() with user_id, caldav_path, ics_hex.""" + mock_message = MagicMock() + mock_message.id = str(uuid.uuid4()) + mock_task.delay.return_value = mock_message + + user = factories.UserFactory(email="dispatch@example.com") + caldav_path = f"/calendars/users/{user.email}/some-uuid/" + + client = APIClient() + client.force_login(user) + + from django.core.files.uploadedfile import ( # noqa: PLC0415 + SimpleUploadedFile, + ) + + ics_file = SimpleUploadedFile( + "events.ics", + b"BEGIN:VCALENDAR\r\nEND:VCALENDAR", + content_type="text/calendar", + ) + response = client.post( + self.IMPORT_URL, + {"file": ics_file, "caldav_path": caldav_path}, + format="multipart", + ) + + assert response.status_code == 202 + assert response.json()["task_id"] == mock_message.id + + mock_task.delay.assert_called_once() + call_args = mock_task.delay.call_args.args + assert call_args[0] == str(user.id) + assert call_args[1] == caldav_path + # Third arg is ics_data as hex + assert bytes.fromhex(call_args[2]) == b"BEGIN:VCALENDAR\r\nEND:VCALENDAR" + + @patch("core.tasks.import_events_task") + def test_api_returns_202_with_task_id(self, mock_task): + """Successful dispatch should return HTTP 202 with task_id.""" + mock_message = MagicMock() + mock_message.id = "test-task-id-123" + mock_task.delay.return_value = mock_message + + user = factories.UserFactory(email="dispatch202@example.com") + caldav_path = f"/calendars/users/{user.email}/cal-uuid/" + + client = APIClient() + client.force_login(user) + + from django.core.files.uploadedfile import ( # noqa: PLC0415 + SimpleUploadedFile, + ) + + ics_file = SimpleUploadedFile( + "events.ics", + b"BEGIN:VCALENDAR\r\nEND:VCALENDAR", + content_type="text/calendar", + ) + response = client.post( + self.IMPORT_URL, + {"file": ics_file, "caldav_path": caldav_path}, + format="multipart", + ) + + assert response.status_code == 202 + data = response.json() + assert data["task_id"] == "test-task-id-123" + + +# --------------------------------------------------------------------------- +# Full round-trip: API dispatch → EagerBroker → poll result +# --------------------------------------------------------------------------- + + +class TestImportFullRoundTrip: + """Full integration: POST import → task runs (EagerBroker) → poll result.""" + + IMPORT_URL = "/api/v1.0/calendars/import-events/" + TASK_URL = "/api/v1.0/tasks/{task_id}/" + + @patch.object( + __import__( + "core.services.import_service", fromlist=["ICSImportService"] + ).ICSImportService, + "import_events", + ) + def test_full_round_trip(self, mock_import): + """POST import → EagerBroker runs task → poll returns SUCCESS.""" + mock_import.return_value = ImportResult( + total_events=2, + imported_count=2, + duplicate_count=0, + skipped_count=0, + errors=[], + ) + + user = factories.UserFactory(email="roundtrip@example.com") + caldav_path = f"/calendars/users/{user.email}/cal-uuid/" + + client = APIClient() + client.force_login(user) + + from django.core.files.uploadedfile import ( # noqa: PLC0415 + SimpleUploadedFile, + ) + + ics_file = SimpleUploadedFile( + "events.ics", + b"BEGIN:VCALENDAR\r\nEND:VCALENDAR", + content_type="text/calendar", + ) + + # Step 1: POST triggers task dispatch + response = client.post( + self.IMPORT_URL, + {"file": ics_file, "caldav_path": caldav_path}, + format="multipart", + ) + assert response.status_code == 202 + task_id = response.json()["task_id"] + + # Step 2: Poll for result + poll_response = client.get(self.TASK_URL.format(task_id=task_id)) + assert poll_response.status_code == 200 + data = poll_response.json() + assert data["status"] == "SUCCESS" + assert data["result"]["total_events"] == 2 + assert data["result"]["imported_count"] == 2 + assert data["error"] is None + + @patch.object( + __import__( + "core.services.import_service", fromlist=["ICSImportService"] + ).ICSImportService, + "import_events", + ) + def test_full_round_trip_with_errors(self, mock_import): + """Task that returns partial failure should surface errors via poll.""" + mock_import.return_value = ImportResult( + total_events=3, + imported_count=1, + duplicate_count=0, + skipped_count=2, + errors=["Event A", "Event B"], + ) + + user = factories.UserFactory(email="roundtrip-err@example.com") + caldav_path = f"/calendars/users/{user.email}/cal-uuid/" + + client = APIClient() + client.force_login(user) + + from django.core.files.uploadedfile import ( # noqa: PLC0415 + SimpleUploadedFile, + ) + + ics_file = SimpleUploadedFile( + "events.ics", + b"BEGIN:VCALENDAR\r\nEND:VCALENDAR", + content_type="text/calendar", + ) + + response = client.post( + self.IMPORT_URL, + {"file": ics_file, "caldav_path": caldav_path}, + format="multipart", + ) + assert response.status_code == 202 + task_id = response.json()["task_id"] + + poll_response = client.get(self.TASK_URL.format(task_id=task_id)) + assert poll_response.status_code == 200 + data = poll_response.json() + assert data["status"] == "SUCCESS" + assert data["result"]["skipped_count"] == 2 + assert data["result"]["errors"] == ["Event A", "Event B"] diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index a4aaeff..e59dbdd 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -8,19 +8,18 @@ from rest_framework.routers import DefaultRouter from core.api import viewsets from core.api.viewsets_caldav import CalDAVProxyView, CalDAVSchedulingCallbackView +from core.api.viewsets_channels import ChannelViewSet from core.api.viewsets_ical import ICalExportView -from core.api.viewsets_rsvp import RSVPView +from core.api.viewsets_rsvp import RSVPConfirmView, RSVPProcessView +from core.api.viewsets_task import TaskDetailView from core.external_api import viewsets as external_api_viewsets # - Main endpoints router = DefaultRouter() router.register("users", viewsets.UserViewSet, basename="users") router.register("calendars", viewsets.CalendarViewSet, basename="calendars") -router.register( - "subscription-tokens", - viewsets.SubscriptionTokenViewSet, - basename="subscription-tokens", -) +router.register("resources", viewsets.ResourceViewSet, basename="resources") +router.register("channels", ChannelViewSet, basename="channels") urlpatterns = [ path( @@ -29,35 +28,46 @@ urlpatterns = [ [ *router.urls, *oidc_urls, - # CalDAV proxy - root path (must come before catch-all to match /caldav exactly) - path("caldav", CalDAVProxyView.as_view(), name="caldav-root"), - path("caldav/", CalDAVProxyView.as_view(), name="caldav-root-slash"), - # CalDAV proxy - catch all paths with content - re_path( - r"^caldav/(?P.+)$", - CalDAVProxyView.as_view(), - name="caldav-proxy", - ), - # CalDAV scheduling callback endpoint (separate from caldav proxy) + # CalDAV scheduling callback endpoint path( "caldav-scheduling-callback/", CalDAVSchedulingCallbackView.as_view(), name="caldav-scheduling-callback", ), + # RSVP POST endpoint (state-changing, with DRF throttling) + path( + "rsvp/", + RSVPProcessView.as_view(), + name="rsvp-process", + ), + # Task status polling endpoint + path( + "tasks//", + TaskDetailView.as_view(), + name="task-detail", + ), ] ), ), path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()), + # CalDAV proxy - top-level stable path (not versioned) + path("caldav", CalDAVProxyView.as_view(), name="caldav-root"), + path("caldav/", CalDAVProxyView.as_view(), name="caldav-root-slash"), + re_path( + r"^caldav/(?P.+)$", + CalDAVProxyView.as_view(), + name="caldav-proxy", + ), # Public iCal export endpoint (no authentication required) - # Token in URL acts as authentication - path( - "ical/.ics", + # base64url channel ID for lookup, base64url token for auth, filename cosmetic + re_path( + r"^ical/(?P[A-Za-z0-9_-]+)/(?P[A-Za-z0-9_-]+)/[^/]+\.ics$", ICalExportView.as_view(), name="ical-export", ), - # RSVP endpoint (no authentication required) + # RSVP GET endpoint (renders auto-submitting confirmation page) # Signed token in query string acts as authentication - path("rsvp/", RSVPView.as_view(), name="rsvp"), + path("rsvp/", RSVPConfirmView.as_view(), name="rsvp"), ] diff --git a/src/backend/e2e/viewsets.py b/src/backend/e2e/viewsets.py index 6150242..6ffe416 100644 --- a/src/backend/e2e/viewsets.py +++ b/src/backend/e2e/viewsets.py @@ -27,11 +27,15 @@ class UserAuthViewSet(drf.viewsets.ViewSet): serializer.is_valid(raise_exception=True) # Create user if doesn't exist - user = models.User.objects.filter( - email=serializer.validated_data["email"] - ).first() + email = serializer.validated_data["email"] + user = models.User.objects.filter(email=email).first() if not user: - user = models.User(email=serializer.validated_data["email"]) + domain = email.split("@")[-1] if "@" in email else "e2e" + org, _ = models.Organization.objects.get_or_create( + external_id=domain, + defaults={"name": domain}, + ) + user = models.User(email=email, organization=org) user.set_unusable_password() user.save() diff --git a/src/backend/locale/de_DE/LC_MESSAGES/django.po b/src/backend/locale/de_DE/LC_MESSAGES/django.po deleted file mode 100644 index 2308ef9..0000000 --- a/src/backend/locale/de_DE/LC_MESSAGES/django.po +++ /dev/null @@ -1,455 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: lasuite-docs\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-19 15:10+0000\n" -"PO-Revision-Date: 2025-01-27 09:27\n" -"Last-Translator: \n" -"Language-Team: German\n" -"Language: de_DE\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Crowdin-Project: lasuite-docs\n" -"X-Crowdin-Project-ID: 754523\n" -"X-Crowdin-Language: de\n" -"X-Crowdin-File: backend-calendars.pot\n" -"X-Crowdin-File-ID: 18\n" - -#: core/admin.py:26 -msgid "Personal info" -msgstr "Persönliche Daten" - -#: core/admin.py:39 core/admin.py:119 -msgid "Permissions" -msgstr "Berechtigungen" - -#: core/admin.py:51 -msgid "Important dates" -msgstr "Wichtige Daten" - -#: core/admin.py:129 -msgid "Tree structure" -msgstr "" - -#: core/api/filters.py:16 -msgid "Title" -msgstr "Titel" - -#: core/api/filters.py:28 -msgid "Creator is me" -msgstr "Ersteller bin ich" - -#: core/api/filters.py:31 -msgid "Favorite" -msgstr "Favorit" - -#: core/api/serializers.py:304 -msgid "An item with this title already exists in the current path." -msgstr "" - -#: core/api/serializers.py:397 -msgid "This field is required for files." -msgstr "" - -#: core/api/serializers.py:409 -msgid "This field is required for folders." -msgstr "" - -#: core/models.py:53 core/models.py:60 -msgid "Reader" -msgstr "Lesen" - -#: core/models.py:54 core/models.py:61 -msgid "Editor" -msgstr "Bearbeiten" - -#: core/models.py:62 -msgid "Administrator" -msgstr "" - -#: core/models.py:63 -msgid "Owner" -msgstr "Besitzer" - -#: core/models.py:74 -msgid "Restricted" -msgstr "Beschränkt" - -#: core/models.py:78 -msgid "Authenticated" -msgstr "Authentifiziert" - -#: core/models.py:80 -msgid "Public" -msgstr "Öffentlich" - -#: core/models.py:86 -msgid "Folder" -msgstr "" - -#: core/models.py:87 -msgid "File" -msgstr "" - -#: core/models.py:93 -msgid "Pending" -msgstr "" - -#: core/models.py:94 -msgid "Uploaded" -msgstr "" - -#: core/models.py:116 -msgid "id" -msgstr "" - -#: core/models.py:117 -msgid "primary key for the record as UUID" -msgstr "" - -#: core/models.py:123 -msgid "created on" -msgstr "Erstellt" - -#: core/models.py:124 -msgid "date and time at which a record was created" -msgstr "Datum und Uhrzeit, an dem ein Datensatz erstellt wurde" - -#: core/models.py:129 -msgid "updated on" -msgstr "Aktualisiert" - -#: core/models.py:130 -msgid "date and time at which a record was last updated" -msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde" - -#: core/models.py:166 -msgid "" -"We couldn't find a user with this sub but the email is already associated " -"with a registered user." -msgstr "" - -#: core/models.py:179 -msgid "" -"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" -"_/: characters." -msgstr "" -"Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, " -"Zahlen und die @/./+/-/_/: Zeichen enthalten." - -#: core/models.py:185 -msgid "sub" -msgstr "unter" - -#: core/models.py:187 -msgid "" -"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: " -"characters only." -msgstr "" -"Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen " -"@/./+/-/_/:" - -#: core/models.py:196 -msgid "full name" -msgstr "Name" - -#: core/models.py:197 -msgid "short name" -msgstr "Kurzbezeichnung" - -#: core/models.py:199 -msgid "identity email address" -msgstr "Identitäts-E-Mail-Adresse" - -#: core/models.py:204 -msgid "admin email address" -msgstr "Admin E-Mail-Adresse" - -#: core/models.py:211 -msgid "language" -msgstr "Sprache" - -#: core/models.py:212 -msgid "The language in which the user wants to see the interface." -msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte." - -#: core/models.py:218 -msgid "The timezone in which the user wants to see times." -msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte." - -#: core/models.py:221 -msgid "device" -msgstr "Gerät" - -#: core/models.py:223 -msgid "Whether the user is a device or a real user." -msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist." - -#: core/models.py:226 -msgid "staff status" -msgstr "Status des Teammitgliedes" - -#: core/models.py:228 -msgid "Whether the user can log into this admin site." -msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann." - -#: core/models.py:231 -msgid "active" -msgstr "aktiviert" - -#: core/models.py:234 -msgid "" -"Whether this user should be treated as active. Unselect this instead of " -"deleting accounts." -msgstr "" -"Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie " -"diese Option, anstatt Konten zu löschen." - -#: core/models.py:246 -msgid "user" -msgstr "Benutzer" - -#: core/models.py:247 -msgid "users" -msgstr "Benutzer" - -#: core/models.py:269 -msgid "Workspace" -msgstr "" - -#: core/models.py:457 -msgid "Only folders can have children." -msgstr "" - -#: core/models.py:470 -#, fuzzy -#| msgid "This team is already in this item." -msgid "title already exists in this folder." -msgstr "Dieses Team befindet sich bereits in diesem Dokument." - -#: core/models.py:504 -msgid "title" -msgstr "Titel" - -#: core/models.py:549 -msgid "Item" -msgstr "" - -#: core/models.py:550 -#, fuzzy -#| msgid "items" -msgid "Items" -msgstr "Dokumente" - -#: core/models.py:815 -#, fuzzy, python-brace-format -#| msgid "{name} shared a item with you!" -msgid "{name} shared an item with you!" -msgstr "{name} hat ein Dokument mit Ihnen geteilt!" - -#: core/models.py:817 -#, python-brace-format -msgid "{name} invited you with the role \"{role}\" on the following item:" -msgstr "" -"{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:" - -#: core/models.py:820 -#, fuzzy, python-brace-format -#| msgid "{name} shared a item with you: {title}" -msgid "{name} shared an item with you: {title}" -msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}" - -#: core/models.py:872 -#, fuzzy -#| msgid "This team is already in this template." -msgid "This item is already hard deleted." -msgstr "Dieses Team ist bereits in diesem Template." - -#: core/models.py:882 -msgid "To hard delete an item, it must first be soft deleted." -msgstr "" - -#: core/models.py:902 -#, fuzzy -#| msgid "This team is already in this template." -msgid "This item is not deleted." -msgstr "Dieses Team ist bereits in diesem Template." - -#: core/models.py:918 -msgid "This item was permanently deleted and cannot be restored." -msgstr "" - -#: core/models.py:968 -msgid "Only folders can be targeted when moving an item" -msgstr "" - -#: core/models.py:1021 -#, fuzzy -#| msgid "item/user link trace" -msgid "Item/user link trace" -msgstr "Dokument/Benutzer Linkverfolgung" - -#: core/models.py:1022 -#, fuzzy -#| msgid "item/user link traces" -msgid "Item/user link traces" -msgstr "Dokument/Benutzer Linkverfolgung" - -#: core/models.py:1028 -msgid "A link trace already exists for this item/user." -msgstr "" - -#: core/models.py:1051 -#, fuzzy -#| msgid "item favorite" -msgid "Item favorite" -msgstr "Dokumentenfavorit" - -#: core/models.py:1052 -#, fuzzy -#| msgid "item favorites" -msgid "Item favorites" -msgstr "Dokumentfavoriten" - -#: core/models.py:1058 -msgid "" -"This item is already targeted by a favorite relation instance for the same " -"user." -msgstr "" -"Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden." - -#: core/models.py:1080 -#, fuzzy -#| msgid "item/user relation" -msgid "Item/user relation" -msgstr "Dokument/Benutzerbeziehung" - -#: core/models.py:1081 -#, fuzzy -#| msgid "item/user relations" -msgid "Item/user relations" -msgstr "Dokument/Benutzerbeziehungen" - -#: core/models.py:1087 -msgid "This user is already in this item." -msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument." - -#: core/models.py:1093 -msgid "This team is already in this item." -msgstr "Dieses Team befindet sich bereits in diesem Dokument." - -#: core/models.py:1099 -msgid "Either user or team must be set, not both." -msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides." - -#: core/models.py:1126 -msgid "email address" -msgstr "E-Mail-Adresse" - -#: core/models.py:1145 -#, fuzzy -#| msgid "item invitation" -msgid "Item invitation" -msgstr "Einladung zum Dokument" - -#: core/models.py:1146 -#, fuzzy -#| msgid "item invitations" -msgid "Item invitations" -msgstr "Dokumenteinladungen" - -#: core/templates/mail/html/invitation.html:162 -#: core/templates/mail/text/invitation.txt:3 -msgid "Logo email" -msgstr "" - -#: core/templates/mail/html/invitation.html:209 -#: core/templates/mail/text/invitation.txt:10 -msgid "Open" -msgstr "" - -#: core/templates/mail/html/invitation.html:226 -#: core/templates/mail/text/invitation.txt:14 -msgid "" -" Calendars, your new essential tool for organizing, sharing and collaborating as " -"a team. " -msgstr "" - -#: core/templates/mail/html/invitation.html:233 -#: core/templates/mail/text/invitation.txt:16 -#, python-format -msgid " Brought to you by %(brandname)s " -msgstr "" - -#: calendars/settings.py:250 -msgid "English" -msgstr "Englisch" - -#: calendars/settings.py:251 -msgid "French" -msgstr "Französisch" - -#: calendars/settings.py:252 -msgid "German" -msgstr "Deutsch" - -#~ msgid "Invalid response format or token verification failed" -#~ msgstr "Ungültiges Antwortformat oder Token-Verifizierung fehlgeschlagen" - -#~ msgid "User account is disabled" -#~ msgstr "Benutzerkonto ist deaktiviert" - -#, fuzzy -#~| msgid "Untitled item" -#~ msgid "Untitled Item" -#~ msgstr "Unbenanntes Dokument" - -#~ msgid "This email is already associated to a registered user." -#~ msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet." - -#~ msgid "A new item was created on your behalf!" -#~ msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!" - -#~ msgid "You have been granted ownership of a new item:" -#~ msgstr "Sie sind Besitzer eines neuen Dokuments:" - -#~ msgid "Body" -#~ msgstr "Inhalt" - -#~ msgid "Body type" -#~ msgstr "Typ" - -#~ msgid "item" -#~ msgstr "Dokument" - -#~ msgid "description" -#~ msgstr "Beschreibung" - -#~ msgid "code" -#~ msgstr "Code" - -#~ msgid "css" -#~ msgstr "CSS" - -#~ msgid "public" -#~ msgstr "öffentlich" - -#~ msgid "Whether this template is public for anyone to use." -#~ msgstr "Ob diese Vorlage für jedermann öffentlich ist." - -#~ msgid "Template" -#~ msgstr "Vorlage" - -#~ msgid "Templates" -#~ msgstr "Vorlagen" - -#~ msgid "Template/user relation" -#~ msgstr "Vorlage/Benutzer-Beziehung" - -#~ msgid "Template/user relations" -#~ msgstr "Vorlage/Benutzerbeziehungen" - -#~ msgid "This user is already in this template." -#~ msgstr "Dieser Benutzer ist bereits in dieser Vorlage." diff --git a/src/backend/locale/en_US/LC_MESSAGES/django.po b/src/backend/locale/en_US/LC_MESSAGES/django.po deleted file mode 100644 index e4d70c6..0000000 --- a/src/backend/locale/en_US/LC_MESSAGES/django.po +++ /dev/null @@ -1,362 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: lasuite-docs\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-19 15:10+0000\n" -"PO-Revision-Date: 2025-01-27 09:27\n" -"Last-Translator: \n" -"Language-Team: English\n" -"Language: en_US\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Crowdin-Project: lasuite-docs\n" -"X-Crowdin-Project-ID: 754523\n" -"X-Crowdin-Language: en\n" -"X-Crowdin-File: backend-calendars.pot\n" -"X-Crowdin-File-ID: 18\n" - -#: core/admin.py:26 -msgid "Personal info" -msgstr "" - -#: core/admin.py:39 core/admin.py:119 -msgid "Permissions" -msgstr "" - -#: core/admin.py:51 -msgid "Important dates" -msgstr "" - -#: core/admin.py:129 -msgid "Tree structure" -msgstr "" - -#: core/api/filters.py:16 -msgid "Title" -msgstr "" - -#: core/api/filters.py:28 -msgid "Creator is me" -msgstr "" - -#: core/api/filters.py:31 -msgid "Favorite" -msgstr "" - -#: core/api/serializers.py:304 -msgid "An item with this title already exists in the current path." -msgstr "" - -#: core/api/serializers.py:397 -msgid "This field is required for files." -msgstr "" - -#: core/api/serializers.py:409 -msgid "This field is required for folders." -msgstr "" - -#: core/models.py:53 core/models.py:60 -msgid "Reader" -msgstr "" - -#: core/models.py:54 core/models.py:61 -msgid "Editor" -msgstr "" - -#: core/models.py:62 -msgid "Administrator" -msgstr "" - -#: core/models.py:63 -msgid "Owner" -msgstr "" - -#: core/models.py:74 -msgid "Restricted" -msgstr "" - -#: core/models.py:78 -msgid "Authenticated" -msgstr "" - -#: core/models.py:80 -msgid "Public" -msgstr "" - -#: core/models.py:86 -msgid "Folder" -msgstr "" - -#: core/models.py:87 -msgid "File" -msgstr "" - -#: core/models.py:93 -msgid "Pending" -msgstr "" - -#: core/models.py:94 -msgid "Uploaded" -msgstr "" - -#: core/models.py:116 -msgid "id" -msgstr "" - -#: core/models.py:117 -msgid "primary key for the record as UUID" -msgstr "" - -#: core/models.py:123 -msgid "created on" -msgstr "" - -#: core/models.py:124 -msgid "date and time at which a record was created" -msgstr "" - -#: core/models.py:129 -msgid "updated on" -msgstr "" - -#: core/models.py:130 -msgid "date and time at which a record was last updated" -msgstr "" - -#: core/models.py:166 -msgid "" -"We couldn't find a user with this sub but the email is already associated " -"with a registered user." -msgstr "" - -#: core/models.py:179 -msgid "" -"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" -"_/: characters." -msgstr "" - -#: core/models.py:185 -msgid "sub" -msgstr "" - -#: core/models.py:187 -msgid "" -"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: " -"characters only." -msgstr "" - -#: core/models.py:196 -msgid "full name" -msgstr "" - -#: core/models.py:197 -msgid "short name" -msgstr "" - -#: core/models.py:199 -msgid "identity email address" -msgstr "" - -#: core/models.py:204 -msgid "admin email address" -msgstr "" - -#: core/models.py:211 -msgid "language" -msgstr "" - -#: core/models.py:212 -msgid "The language in which the user wants to see the interface." -msgstr "" - -#: core/models.py:218 -msgid "The timezone in which the user wants to see times." -msgstr "" - -#: core/models.py:221 -msgid "device" -msgstr "" - -#: core/models.py:223 -msgid "Whether the user is a device or a real user." -msgstr "" - -#: core/models.py:226 -msgid "staff status" -msgstr "" - -#: core/models.py:228 -msgid "Whether the user can log into this admin site." -msgstr "" - -#: core/models.py:231 -msgid "active" -msgstr "" - -#: core/models.py:234 -msgid "" -"Whether this user should be treated as active. Unselect this instead of " -"deleting accounts." -msgstr "" - -#: core/models.py:246 -msgid "user" -msgstr "" - -#: core/models.py:247 -msgid "users" -msgstr "" - -#: core/models.py:269 -msgid "Workspace" -msgstr "" - -#: core/models.py:457 -msgid "Only folders can have children." -msgstr "" - -#: core/models.py:470 -msgid "title already exists in this folder." -msgstr "" - -#: core/models.py:504 -msgid "title" -msgstr "" - -#: core/models.py:549 -msgid "Item" -msgstr "" - -#: core/models.py:550 -msgid "Items" -msgstr "" - -#: core/models.py:815 -#, python-brace-format -msgid "{name} shared an item with you!" -msgstr "" - -#: core/models.py:817 -#, python-brace-format -msgid "{name} invited you with the role \"{role}\" on the following item:" -msgstr "" - -#: core/models.py:820 -#, python-brace-format -msgid "{name} shared an item with you: {title}" -msgstr "" - -#: core/models.py:872 -msgid "This item is already hard deleted." -msgstr "" - -#: core/models.py:882 -msgid "To hard delete an item, it must first be soft deleted." -msgstr "" - -#: core/models.py:902 -msgid "This item is not deleted." -msgstr "" - -#: core/models.py:918 -msgid "This item was permanently deleted and cannot be restored." -msgstr "" - -#: core/models.py:968 -msgid "Only folders can be targeted when moving an item" -msgstr "" - -#: core/models.py:1021 -msgid "Item/user link trace" -msgstr "" - -#: core/models.py:1022 -msgid "Item/user link traces" -msgstr "" - -#: core/models.py:1028 -msgid "A link trace already exists for this item/user." -msgstr "" - -#: core/models.py:1051 -msgid "Item favorite" -msgstr "" - -#: core/models.py:1052 -msgid "Item favorites" -msgstr "" - -#: core/models.py:1058 -msgid "" -"This item is already targeted by a favorite relation instance for the same " -"user." -msgstr "" - -#: core/models.py:1080 -msgid "Item/user relation" -msgstr "" - -#: core/models.py:1081 -msgid "Item/user relations" -msgstr "" - -#: core/models.py:1087 -msgid "This user is already in this item." -msgstr "" - -#: core/models.py:1093 -msgid "This team is already in this item." -msgstr "" - -#: core/models.py:1099 -msgid "Either user or team must be set, not both." -msgstr "" - -#: core/models.py:1126 -msgid "email address" -msgstr "" - -#: core/models.py:1145 -msgid "Item invitation" -msgstr "" - -#: core/models.py:1146 -msgid "Item invitations" -msgstr "" - -#: core/templates/mail/html/invitation.html:162 -#: core/templates/mail/text/invitation.txt:3 -msgid "Logo email" -msgstr "" - -#: core/templates/mail/html/invitation.html:209 -#: core/templates/mail/text/invitation.txt:10 -msgid "Open" -msgstr "" - -#: core/templates/mail/html/invitation.html:226 -#: core/templates/mail/text/invitation.txt:14 -msgid "" -" Calendars, your new essential tool for organizing, sharing and collaborating as " -"a team. " -msgstr "" - -#: core/templates/mail/html/invitation.html:233 -#: core/templates/mail/text/invitation.txt:16 -#, python-format -msgid " Brought to you by %(brandname)s " -msgstr "" - -#: calendars/settings.py:250 -msgid "English" -msgstr "" - -#: calendars/settings.py:251 -msgid "French" -msgstr "" - -#: calendars/settings.py:252 -msgid "German" -msgstr "" diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.po b/src/backend/locale/fr_FR/LC_MESSAGES/django.po deleted file mode 100644 index 88ce6cb..0000000 --- a/src/backend/locale/fr_FR/LC_MESSAGES/django.po +++ /dev/null @@ -1,371 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: lasuite-docs\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-19 15:10+0000\n" -"PO-Revision-Date: 2025-01-27 09:27\n" -"Last-Translator: \n" -"Language-Team: French\n" -"Language: fr_FR\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Crowdin-Project: lasuite-docs\n" -"X-Crowdin-Project-ID: 754523\n" -"X-Crowdin-Language: fr\n" -"X-Crowdin-File: backend-calendars.pot\n" -"X-Crowdin-File-ID: 18\n" - -#: core/admin.py:26 -msgid "Personal info" -msgstr "Infos Personnelles" - -#: core/admin.py:39 core/admin.py:119 -msgid "Permissions" -msgstr "" - -#: core/admin.py:51 -msgid "Important dates" -msgstr "Dates importantes" - -#: core/admin.py:129 -msgid "Tree structure" -msgstr "" - -#: core/api/filters.py:16 -msgid "Title" -msgstr "" - -#: core/api/filters.py:28 -msgid "Creator is me" -msgstr "" - -#: core/api/filters.py:31 -msgid "Favorite" -msgstr "" - -#: core/api/serializers.py:304 -msgid "An item with this title already exists in the current path." -msgstr "" - -#: core/api/serializers.py:397 -msgid "This field is required for files." -msgstr "" - -#: core/api/serializers.py:409 -msgid "This field is required for folders." -msgstr "" - -#: core/models.py:53 core/models.py:60 -msgid "Reader" -msgstr "Lecteur" - -#: core/models.py:54 core/models.py:61 -msgid "Editor" -msgstr "Éditeur" - -#: core/models.py:62 -msgid "Administrator" -msgstr "Administrateur" - -#: core/models.py:63 -msgid "Owner" -msgstr "Propriétaire" - -#: core/models.py:74 -msgid "Restricted" -msgstr "Restreint" - -#: core/models.py:78 -msgid "Authenticated" -msgstr "Authentifié" - -#: core/models.py:80 -msgid "Public" -msgstr "" - -#: core/models.py:86 -msgid "Folder" -msgstr "" - -#: core/models.py:87 -msgid "File" -msgstr "" - -#: core/models.py:93 -msgid "Pending" -msgstr "" - -#: core/models.py:94 -msgid "Uploaded" -msgstr "" - -#: core/models.py:116 -msgid "id" -msgstr "" - -#: core/models.py:117 -msgid "primary key for the record as UUID" -msgstr "" - -#: core/models.py:123 -msgid "created on" -msgstr "" - -#: core/models.py:124 -msgid "date and time at which a record was created" -msgstr "" - -#: core/models.py:129 -msgid "updated on" -msgstr "" - -#: core/models.py:130 -msgid "date and time at which a record was last updated" -msgstr "" - -#: core/models.py:166 -msgid "" -"We couldn't find a user with this sub but the email is already associated " -"with a registered user." -msgstr "" - -#: core/models.py:179 -msgid "" -"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" -"_/: characters." -msgstr "" - -#: core/models.py:185 -msgid "sub" -msgstr "" - -#: core/models.py:187 -msgid "" -"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: " -"characters only." -msgstr "" - -#: core/models.py:196 -msgid "full name" -msgstr "" - -#: core/models.py:197 -msgid "short name" -msgstr "" - -#: core/models.py:199 -msgid "identity email address" -msgstr "" - -#: core/models.py:204 -msgid "admin email address" -msgstr "" - -#: core/models.py:211 -msgid "language" -msgstr "" - -#: core/models.py:212 -msgid "The language in which the user wants to see the interface." -msgstr "" - -#: core/models.py:218 -msgid "The timezone in which the user wants to see times." -msgstr "" - -#: core/models.py:221 -msgid "device" -msgstr "" - -#: core/models.py:223 -msgid "Whether the user is a device or a real user." -msgstr "" - -#: core/models.py:226 -msgid "staff status" -msgstr "" - -#: core/models.py:228 -msgid "Whether the user can log into this admin site." -msgstr "" - -#: core/models.py:231 -msgid "active" -msgstr "" - -#: core/models.py:234 -msgid "" -"Whether this user should be treated as active. Unselect this instead of " -"deleting accounts." -msgstr "" - -#: core/models.py:246 -msgid "user" -msgstr "" - -#: core/models.py:247 -msgid "users" -msgstr "" - -#: core/models.py:269 -msgid "Workspace" -msgstr "" - -#: core/models.py:457 -msgid "Only folders can have children." -msgstr "" - -#: core/models.py:470 -msgid "title already exists in this folder." -msgstr "" - -#: core/models.py:504 -msgid "title" -msgstr "" - -#: core/models.py:549 -msgid "Item" -msgstr "" - -#: core/models.py:550 -msgid "Items" -msgstr "" - -#: core/models.py:815 -#, python-brace-format -msgid "{name} shared an item with you!" -msgstr "{name} a partagé un item avec vous!" - -#: core/models.py:817 -#, python-brace-format -msgid "{name} invited you with the role \"{role}\" on the following item:" -msgstr "{name} vous a invité avec le rôle \"{role}\" sur le item suivant:" - -#: core/models.py:820 -#, python-brace-format -#| msgid "{name} shared an item with you: {title}" -msgid "{name} shared an item with you: {title}" -msgstr "{name} a partagé un item avec vous: {title}" - -#: core/models.py:872 -msgid "This item is already hard deleted." -msgstr "" - -#: core/models.py:882 -msgid "To hard delete an item, it must first be soft deleted." -msgstr "" - -#: core/models.py:902 -msgid "This item is not deleted." -msgstr "" - -#: core/models.py:918 -msgid "This item was permanently deleted and cannot be restored." -msgstr "" - -#: core/models.py:968 -msgid "Only folders can be targeted when moving an item" -msgstr "" - -#: core/models.py:1021 -msgid "Item/user link trace" -msgstr "" - -#: core/models.py:1022 -msgid "Item/user link traces" -msgstr "" - -#: core/models.py:1028 -msgid "A link trace already exists for this item/user." -msgstr "" - -#: core/models.py:1051 -msgid "Item favorite" -msgstr "" - -#: core/models.py:1052 -msgid "Item favorites" -msgstr "" - -#: core/models.py:1058 -msgid "" -"This item is already targeted by a favorite relation instance for the same " -"user." -msgstr "" - -#: core/models.py:1080 -msgid "Item/user relation" -msgstr "" - -#: core/models.py:1081 -msgid "Item/user relations" -msgstr "" - -#: core/models.py:1087 -msgid "This user is already in this item." -msgstr "" - -#: core/models.py:1093 -msgid "This team is already in this item." -msgstr "" - -#: core/models.py:1099 -msgid "Either user or team must be set, not both." -msgstr "" - -#: core/models.py:1126 -msgid "email address" -msgstr "" - -#: core/models.py:1145 -msgid "Item invitation" -msgstr "" - -#: core/models.py:1146 -msgid "Item invitations" -msgstr "" - -#: core/templates/mail/html/invitation.html:162 -#: core/templates/mail/text/invitation.txt:3 -msgid "Logo email" -msgstr "" - -#: core/templates/mail/html/invitation.html:209 -#: core/templates/mail/text/invitation.txt:10 -msgid "Open" -msgstr "Ouvrir" - -#: core/templates/mail/html/invitation.html:226 -#: core/templates/mail/text/invitation.txt:14 -msgid "" -" Calendars, your new essential tool for organizing, sharing and collaborating as " -"a team. " -msgstr "" -"Fichiers, votre outil essentiel pour organiser, partager et collaborer en " -"équipe." - -#: core/templates/mail/html/invitation.html:233 -#: core/templates/mail/text/invitation.txt:16 -#, python-format -msgid " Brought to you by %(brandname)s " -msgstr " Proposé par %(brandname)s " - -#: calendars/settings.py:250 -msgid "English" -msgstr "" - -#: calendars/settings.py:251 -msgid "French" -msgstr "" - -#: calendars/settings.py:252 -msgid "German" -msgstr "" - -#~ msgid "A new item was created on your behalf!" -#~ msgstr "Un nouveau item a été créé pour vous !" - -#~ msgid "You have been granted ownership of a new item:" -#~ msgstr "Vous avez été déclaré propriétaire d'un nouveau item :" diff --git a/src/backend/locale/nl_NL/LC_MESSAGES/django.po b/src/backend/locale/nl_NL/LC_MESSAGES/django.po deleted file mode 100644 index 48e39e2..0000000 --- a/src/backend/locale/nl_NL/LC_MESSAGES/django.po +++ /dev/null @@ -1,369 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: lasuite-docs\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-19 15:10+0000\n" -"PO-Revision-Date: 2025-01-27 09:27\n" -"Last-Translator: \n" -"Language-Team: Dutch\n" -"Language: nl_NL\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Crowdin-Project: lasuite-docs\n" -"X-Crowdin-Project-ID: 754523\n" -"X-Crowdin-Language: nl\n" -"X-Crowdin-File: backend-calendars.pot\n" -"X-Crowdin-File-ID: 18\n" - -#: core/admin.py:26 -msgid "Personal info" -msgstr "Persoonlijke gegevens" - -#: core/admin.py:39 core/admin.py:119 -msgid "Permissions" -msgstr "Machtigingen" - -#: core/admin.py:51 -msgid "Important dates" -msgstr "Belangrijke data" - -#: core/admin.py:129 -msgid "Tree structure" -msgstr "Boomstructuur" - -#: core/api/filters.py:16 -msgid "Title" -msgstr "Titel" - -#: core/api/filters.py:28 -msgid "Creator is me" -msgstr "Ik ben eigenaar" - -#: core/api/filters.py:31 -msgid "Favorite" -msgstr "Favoriet" - -#: core/api/serializers.py:304 -msgid "An item with this title already exists in the current path." -msgstr "Er bestaat al een item met deze titel in het huidige pad." - -#: core/api/serializers.py:397 -msgid "This field is required for files." -msgstr "Dit veld is verplicht voor bestanden" - -#: core/api/serializers.py:409 -msgid "This field is required for folders." -msgstr "Dit veld is verplicht voor mappen." - -#: core/models.py:53 core/models.py:60 -msgid "Reader" -msgstr "Lezer" - -#: core/models.py:54 core/models.py:61 -msgid "Editor" -msgstr "Redacteur" - -#: core/models.py:62 -msgid "Administrator" -msgstr "Beheerder" - -#: core/models.py:63 -msgid "Owner" -msgstr "Eigenaar" - -#: core/models.py:74 -msgid "Restricted" -msgstr "Beperkt" - -#: core/models.py:78 -msgid "Authenticated" -msgstr "Ingelogd" - -#: core/models.py:80 -msgid "Public" -msgstr "Openbaar" - -#: core/models.py:86 -msgid "Folder" -msgstr "Map" - -#: core/models.py:87 -msgid "File" -msgstr "Bestand" - -#: core/models.py:93 -msgid "Pending" -msgstr "In afwachting" - -#: core/models.py:94 -msgid "Uploaded" -msgstr "Geüpload" - -#: core/models.py:116 -msgid "id" -msgstr "id" - -#: core/models.py:117 -msgid "primary key for the record as UUID" -msgstr "primaire sleutel voor het record als UUID" - -#: core/models.py:123 -msgid "created on" -msgstr "gecreëerd op" - -#: core/models.py:124 -msgid "date and time at which a record was created" -msgstr "datum en tijd waarop een record is gemaakt" - -#: core/models.py:129 -msgid "updated on" -msgstr "bijgewerkt op" - -#: core/models.py:130 -msgid "date and time at which a record was last updated" -msgstr "datum en tijd waarop een record voor het laatst is bijgewerkt" - -#: core/models.py:166 -msgid "" -"We couldn't find a user with this sub but the email is already associated " -"with a registered user." -msgstr "We konden geen gebruiker vinden met deze id, maar het e-mailadres is al gekoppeld aan een geregistreerde gebruiker." - -#: core/models.py:179 -msgid "" -"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/" -"_/: characters." -msgstr "Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/_/: " -"tekens bevatten." - -#: core/models.py:185 -msgid "sub" -msgstr "id" - -#: core/models.py:187 -msgid "" -"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: " -"characters only." -msgstr "Verplicht. 255 tekens of minder. Alleen letters, cijfers en @/./+/-/_/: tekens." - -#: core/models.py:196 -msgid "full name" -msgstr "volledige naam" - -#: core/models.py:197 -msgid "short name" -msgstr "gebruikersnaam" - -#: core/models.py:199 -msgid "identity email address" -msgstr "identiteits e-mailadres" - -#: core/models.py:204 -msgid "admin email address" -msgstr "admin e-mailadres" - -#: core/models.py:211 -msgid "language" -msgstr "taal" - -#: core/models.py:212 -msgid "The language in which the user wants to see the interface." -msgstr "De taal waarin de gebruiker de interface wil zien." - -#: core/models.py:218 -msgid "The timezone in which the user wants to see times." -msgstr "Tijdzone waarin de gebruiker de tijd wil zien." - -#: core/models.py:221 -msgid "device" -msgstr "apparaat" - -#: core/models.py:223 -msgid "Whether the user is a device or a real user." -msgstr "Of de gebruiker een apparaat of een echte gebruiker is." - -#: core/models.py:226 -msgid "staff status" -msgstr "personeelsstatus" - -#: core/models.py:228 -msgid "Whether the user can log into this admin site." -msgstr "Of de gebruiker kan inloggen op deze beheer site." - -#: core/models.py:231 -msgid "active" -msgstr "actief" - -#: core/models.py:234 -msgid "" -"Whether this user should be treated as active. Unselect this instead of " -"deleting accounts." -msgstr "Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in plaats van " -"accounts te verwijderen." - -#: core/models.py:246 -msgid "user" -msgstr "gebruiker" - -#: core/models.py:247 -msgid "users" -msgstr "gebruikers" - -#: core/models.py:269 -msgid "Workspace" -msgstr "Werkruimte" - -#: core/models.py:457 -msgid "Only folders can have children." -msgstr "Alleen mappen kunnen subitems hebben." - -#: core/models.py:470 -msgid "title already exists in this folder." -msgstr "titel bestaat al in deze map." - -#: core/models.py:504 -msgid "title" -msgstr "titel" - -#: core/models.py:549 -msgid "Item" -msgstr "Item" - -#: core/models.py:550 -msgid "Items" -msgstr "Items" - -#: core/models.py:815 -#, python-brace-format -msgid "{name} shared an item with you!" -msgstr "{name} heeft een item met je gedeeld!" - -#: core/models.py:817 -#, python-brace-format -msgid "{name} invited you with the role \"{role}\" on the following item:" -msgstr "{name} heeft je uitgenodigd met de rol \"{role}\" voor het volgende item:" - -#: core/models.py:820 -#, python-brace-format -msgid "{name} shared an item with you: {title}" -msgstr "{name} heeft een item met je gedeeld: {title}" - -#: core/models.py:872 -msgid "This item is already hard deleted." -msgstr "Dit item is al permanent verwijderd." - -#: core/models.py:882 -msgid "To hard delete an item, it must first be soft deleted." -msgstr "Om een item permanent te verwijderen, moet het eerst tijdelijk worden verwijderd." - -#: core/models.py:902 -msgid "This item is not deleted." -msgstr "Dit item is niet verwijderd." - -#: core/models.py:918 -msgid "This item was permanently deleted and cannot be restored." -msgstr "Dit item is permanent verwijderd en kan niet worden hersteld." - -#: core/models.py:968 -msgid "Only folders can be targeted when moving an item" -msgstr "Alleen mappen kunnen worden geselecteerd bij het verplaatsen van een item." - -#: core/models.py:1021 -msgid "Item/user link trace" -msgstr "Item/gebruiker link " - -#: core/models.py:1022 -msgid "Item/user link traces" -msgstr "Item/gebruiker link" - -#: core/models.py:1028 -msgid "A link trace already exists for this item/user." -msgstr "Er bestaat al een link trace voor dit item/gebruiker." - -#: core/models.py:1051 -msgid "Item favorite" -msgstr "Item favoriet" - -#: core/models.py:1052 -msgid "Item favorites" -msgstr "Item favorieten" - -#: core/models.py:1058 -msgid "" -"This item is already targeted by a favorite relation instance for the same " -"user." -msgstr "Dit item is al het doel van een favorietrelatie voor dezelfde " -"gebruiker." - -#: core/models.py:1080 -msgid "Item/user relation" -msgstr "Item/gebruiker relatie" - -#: core/models.py:1081 -msgid "Item/user relations" -msgstr "Item/gebruiker relaties" - -#: core/models.py:1087 -msgid "This user is already in this item." -msgstr "Deze gebruiker bestaat al in dit item." - -#: core/models.py:1093 -msgid "This team is already in this item." -msgstr "Dit team bestaat al in dit item." - -#: core/models.py:1099 -msgid "Either user or team must be set, not both." -msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide." - -#: core/models.py:1126 -msgid "email address" -msgstr "e-mailadres" - -#: core/models.py:1145 -msgid "Item invitation" -msgstr "Item uitnodiging" - -#: core/models.py:1146 -msgid "Item invitations" -msgstr "Item uitnodigingen" - -#: core/templates/mail/html/invitation.html:162 -#: core/templates/mail/text/invitation.txt:3 -msgid "Logo email" -msgstr "Logo e-mail" - -#: core/templates/mail/html/invitation.html:209 -#: core/templates/mail/text/invitation.txt:10 -msgid "Open" -msgstr "Open" - -#: core/templates/mail/html/invitation.html:226 -#: core/templates/mail/text/invitation.txt:14 -msgid "" -" Calendars, your new essential tool for organizing, sharing and collaborating as " -"a team. " -msgstr " Calendars, jouw nieuwe essentiële tool voor het organiseren, delen en samenwerken als team" - -#: core/templates/mail/html/invitation.html:233 -#: core/templates/mail/text/invitation.txt:16 -#, python-format -msgid " Brought to you by %(brandname)s " -msgstr " Aangeboden door %(brandname)s " - -#: calendars/settings.py:250 -msgid "English" -msgstr "Engels" - -#: calendars/settings.py:251 -msgid "French" -msgstr "Frans" - -#: calendars/settings.py:252 -msgid "German" -msgstr "Duits" - -#: calendars/settings.py:253 -msgid "Dutch" -msgstr "Nederlands" diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 7e103e8..8eb9bec 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -28,10 +28,11 @@ dependencies = [ "Brotli==1.2.0", "dj-database-url==3.0.1", "caldav==2.2.3", - "celery[redis]==5.6.0", + "dramatiq[redis]==1.17.1", "django==5.2.9", - "django-celery-beat==2.8.1", + "django-dramatiq==0.12.0", "django-configurations==2.5.1", + "django-fernet-encrypted-fields>=0.2", "django-cors-headers==4.9.0", "django-countries==8.2.0", "django-filter==25.2", @@ -138,6 +139,9 @@ addopts = [ "term-missing", # Allow test files to have the same name in different directories. "--import-mode=importlib", + # Group CalDAV E2E tests on a single worker to avoid concurrent + # access issues with the shared SabreDAV server. + "--dist=loadgroup", ] python_files = [ "test_*.py", diff --git a/src/backend/uv.lock b/src/backend/uv.lock index 75a22da..026b339 100644 --- a/src/backend/uv.lock +++ b/src/backend/uv.lock @@ -101,13 +101,13 @@ source = { editable = "." } dependencies = [ { name = "brotli" }, { name = "caldav" }, - { name = "celery", extra = ["redis"] }, { name = "dj-database-url" }, { name = "django" }, - { name = "django-celery-beat" }, { name = "django-configurations" }, { name = "django-cors-headers" }, { name = "django-countries" }, + { name = "django-dramatiq" }, + { name = "django-fernet-encrypted-fields" }, { name = "django-filter" }, { name = "django-lasuite", extra = ["all"] }, { name = "django-parler" }, @@ -115,6 +115,7 @@ dependencies = [ { name = "django-timezone-field" }, { name = "djangorestframework" }, { name = "djangorestframework-api-key" }, + { name = "dramatiq", extra = ["redis"] }, { name = "drf-spectacular" }, { name = "drf-standardized-errors" }, { name = "factory-boy" }, @@ -157,15 +158,15 @@ dev = [ requires-dist = [ { name = "brotli", specifier = "==1.2.0" }, { name = "caldav", specifier = "==2.2.3" }, - { name = "celery", extras = ["redis"], specifier = "==5.6.0" }, { name = "dj-database-url", specifier = "==3.0.1" }, { name = "django", specifier = "==5.2.9" }, - { name = "django-celery-beat", specifier = "==2.8.1" }, { name = "django-configurations", specifier = "==2.5.1" }, { name = "django-cors-headers", specifier = "==4.9.0" }, { name = "django-countries", specifier = "==8.2.0" }, { name = "django-debug-toolbar", marker = "extra == 'dev'", specifier = "==6.1.0" }, + { name = "django-dramatiq", specifier = "==0.12.0" }, { name = "django-extensions", marker = "extra == 'dev'", specifier = "==4.1" }, + { name = "django-fernet-encrypted-fields", specifier = ">=0.2" }, { name = "django-filter", specifier = "==25.2" }, { name = "django-lasuite", extras = ["all"], specifier = "==0.0.21" }, { name = "django-parler", specifier = "==2.3" }, @@ -173,6 +174,7 @@ requires-dist = [ { name = "django-timezone-field", specifier = ">=5.1" }, { name = "djangorestframework", specifier = "==3.16.1" }, { name = "djangorestframework-api-key", specifier = "==3.1.0" }, + { name = "dramatiq", extras = ["redis"], specifier = "==1.17.1" }, { name = "drf-spectacular", specifier = "==0.29.0" }, { name = "drf-spectacular-sidecar", marker = "extra == 'dev'", specifier = "==2025.12.1" }, { name = "drf-standardized-errors", specifier = "==0.15.0" }, @@ -227,11 +229,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/4e/53a125038d6a814491a0ae3457435c13cf8821eb602292cf9db37ce35f62/celery-5.6.0-py3-none-any.whl", hash = "sha256:33cf01477b175017fc8f22c5ee8a65157591043ba8ca78a443fe703aa910f581", size = 444561, upload-time = "2025-11-30T17:39:44.314Z" }, ] -[package.optional-dependencies] -redis = [ - { name = "kombu", extra = ["redis"] }, -] - [[package]] name = "certifi" version = "2025.11.12" @@ -382,18 +379,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, ] -[[package]] -name = "cron-descriptor" -version = "2.0.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7c/31/0b21d1599656b2ffa6043e51ca01041cd1c0f6dacf5a3e2b620ed120e7d8/cron_descriptor-2.0.6.tar.gz", hash = "sha256:e39d2848e1d8913cfb6e3452e701b5eec662ee18bea8cc5aa53ee1a7bb217157", size = 49456, upload-time = "2025-09-03T16:30:22.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/cc/361326a54ad92e2e12845ad15e335a4e14b8953665007fb514d3393dfb0f/cron_descriptor-2.0.6-py3-none-any.whl", hash = "sha256:3a1c0d837c0e5a32e415f821b36cf758eb92d510e6beff8fbfe4fa16573d93d6", size = 74446, upload-time = "2025-09-03T16:30:21.397Z" }, -] - [[package]] name = "cryptography" version = "46.0.3" @@ -479,23 +464,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/b0/7f42bfc38b8f19b78546d47147e083ed06e12fc29c42da95655e0962c6c2/django-5.2.9-py3-none-any.whl", hash = "sha256:3a4ea88a70370557ab1930b332fd2887a9f48654261cdffda663fef5976bb00a", size = 8290652, upload-time = "2025-12-02T14:01:03.485Z" }, ] -[[package]] -name = "django-celery-beat" -version = "2.8.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "celery" }, - { name = "cron-descriptor" }, - { name = "django" }, - { name = "django-timezone-field" }, - { name = "python-crontab" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/11/0c8b412869b4fda72828572068312b10aafe7ccef7b41af3633af31f9d4b/django_celery_beat-2.8.1.tar.gz", hash = "sha256:dfad0201c0ac50c91a34700ef8fa0a10ee098cc7f3375fe5debed79f2204f80a", size = 175802, upload-time = "2025-05-13T06:58:29.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/e5/3a0167044773dee989b498e9a851fc1663bea9ab879f1179f7b8a827ac10/django_celery_beat-2.8.1-py3-none-any.whl", hash = "sha256:da2b1c6939495c05a551717509d6e3b79444e114a027f7b77bf3727c2a39d171", size = 104833, upload-time = "2025-05-13T06:58:27.309Z" }, -] - [[package]] name = "django-configurations" version = "2.5.1" @@ -547,6 +515,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/72/685c978af45ad08257e2c69687a873eda6b6531c79b6e6091794c41c5ff6/django_debug_toolbar-6.1.0-py3-none-any.whl", hash = "sha256:e214dea4494087e7cebdcea84223819c5eb97f9de3110a3665ad673f0ba98413", size = 269069, upload-time = "2025-10-30T19:50:37.71Z" }, ] +[[package]] +name = "django-dramatiq" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "dramatiq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/32/cd8b1394be24a5a1c0fb213c3ee3e575414159d03f3d09c3699b09162dc6/django_dramatiq-0.12.0.tar.gz", hash = "sha256:d4f4a6ecccb104b10b2b743052a703b7749cd671d492a0f6f2a7e13e846923a8", size = 14262, upload-time = "2024-12-29T12:46:56.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/26/bc518333e9c62bc9c6c11994342b07933537adcb2dbdf6b4fb7ac0a9d88a/django_dramatiq-0.12.0-py3-none-any.whl", hash = "sha256:f9b8fff9510e7e780e993fcd3b8fd31c503c3caa09a535cbead284fb4b636262", size = 12082, upload-time = "2024-12-29T12:46:55.093Z" }, +] + [[package]] name = "django-extensions" version = "4.1" @@ -559,6 +540,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" }, ] +[[package]] +name = "django-fernet-encrypted-fields" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/aa/529af3888215b8a660fc3897d6d63eaf1de9aa0699c633ca0ec483d4361c/django_fernet_encrypted_fields-0.3.1.tar.gz", hash = "sha256:5ed328c7f9cc7f2d452bb2e125f3ea2bea3563a259fa943e5a1c626175889a71", size = 5265, upload-time = "2025-11-10T08:39:57.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/7f/4e0b7ed8413fa58e7a77017342e8ab0e977d41cfc376ab9180ae75f216ec/django_fernet_encrypted_fields-0.3.1-py3-none-any.whl", hash = "sha256:3bd2abab02556dc6e15a58a61161ee6c5cdf45a50a8a52d9e035009eb54c6442", size = 5484, upload-time = "2025-11-10T08:39:55.866Z" }, +] + [[package]] name = "django-filter" version = "25.2" @@ -664,6 +658,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "dramatiq" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/7a/6792ddc64a77d22bfd97261b751a7a76cf2f9d62edc59aafb679ac48b77d/dramatiq-1.17.1.tar.gz", hash = "sha256:2675d2f57e0d82db3a7d2a60f1f9c536365349db78c7f8d80a63e4c54697647a", size = 99071, upload-time = "2024-10-26T05:09:28.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/36/925c7afd5db4f1a3f00676b9c3c58f31ff7ae29a347282d86c8d429280a5/dramatiq-1.17.1-py3-none-any.whl", hash = "sha256:951cdc334478dff8e5150bb02a6f7a947d215ee24b5aedaf738eff20e17913df", size = 120382, upload-time = "2024-10-26T05:09:26.436Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + [[package]] name = "drf-spectacular" version = "0.29.0" @@ -1024,11 +1035,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409", size = 214141, upload-time = "2025-11-25T11:07:31.54Z" }, ] -[package.optional-dependencies] -redis = [ - { name = "redis" }, -] - [[package]] name = "lxml" version = "6.0.2" @@ -1201,6 +1207,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952, upload-time = "2018-07-01T01:42:36.496Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1414,15 +1429,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "python-crontab" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/7f/c54fb7e70b59844526aa4ae321e927a167678660ab51dda979955eafb89a/python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b", size = 57626, upload-time = "2025-07-13T20:05:35.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/42/bb4afa5b088f64092036221843fc989b7db9d9d302494c1f8b024ee78a46/python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884", size = 27533, upload-time = "2025-07-13T20:05:34.266Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" diff --git a/src/backend/worker.py b/src/backend/worker.py new file mode 100644 index 0000000..4ee640e --- /dev/null +++ b/src/backend/worker.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# pylint: disable=import-outside-toplevel +""" +Background task worker with sensible queue defaults. + +Usage: + python worker.py # Process all queues + python worker.py --queues=import,default # Process only specific queues + python worker.py --exclude=sync # Process all except sync + python worker.py --concurrency=4 # Set worker concurrency + python worker.py -v 2 # Verbose logging + +Queue priority order (highest to lowest): + 1. default - General / high-priority tasks + 2. import - File import processing + 3. sync - Background sync tasks +""" + +import argparse +import logging +import multiprocessing +import os +import sys + +# Workaround for Dramatiq + Python 3.14: forkserver (the new default) breaks +# Dramatiq's Canteen shared-memory mechanism, causing worker processes to never +# consume messages. See https://github.com/Bogdanp/dramatiq/issues/701 +# Must be set before dramatiq.cli.main() spawns worker processes. +multiprocessing.set_start_method("fork", force=True) + +# Setup Django before importing the task runner +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "calendars.settings") +os.environ.setdefault("DJANGO_CONFIGURATION", "Development") + +# Override $APP if set by the host (e.g. Scalingo) +os.environ.pop("APP", None) + +from configurations.importer import install # pylint: disable=wrong-import-position + +install(check_options=True) + +import django # pylint: disable=wrong-import-position + +django.setup() + +# Queue definitions in priority order +ALL_QUEUES = ["default", "import", "sync"] +DEFAULT_QUEUES = ALL_QUEUES + + +def get_default_concurrency(): + """Get default concurrency from environment variables.""" + env_value = os.environ.get("WORKER_CONCURRENCY") + if env_value: + try: + return int(env_value) + except ValueError: + return None + return None + + +def discover_tasks_modules(): + """Discover task modules the same way django_dramatiq does.""" + import importlib # noqa: PLC0415 # pylint: disable=wrong-import-position + + from django.apps import ( # noqa: PLC0415 # pylint: disable=wrong-import-position + apps, + ) + from django.conf import ( # noqa: PLC0415 # pylint: disable=wrong-import-position + settings, + ) + from django.utils.module_loading import ( # noqa: PLC0415 # pylint: disable=wrong-import-position + module_has_submodule, + ) + + task_module_names = settings.DRAMATIQ_AUTODISCOVER_MODULES + modules = ["django_dramatiq.setup"] + + for conf in apps.get_app_configs(): + if conf.name == "django_dramatiq": + module = conf.name + ".tasks" + importlib.import_module(module) + logging.getLogger(__name__).info("Discovered tasks module: %r", module) + modules.append(module) + else: + for task_module in task_module_names: + if module_has_submodule(conf.module, task_module): + module = conf.name + "." + task_module + importlib.import_module(module) + logging.getLogger(__name__).info( + "Discovered tasks module: %r", module + ) + modules.append(module) + + return modules + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Start a background task worker.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "--queues", + "-Q", + type=str, + default=None, + help=( + "Comma-separated list of queues to process. " + f"Default: {','.join(DEFAULT_QUEUES)}" + ), + ) + parser.add_argument( + "--exclude", + "-X", + type=str, + default=None, + help="Comma-separated list of queues to exclude.", + ) + parser.add_argument( + "--concurrency", + "-c", + type=int, + default=get_default_concurrency(), + help="Number of worker processes. Default: WORKER_CONCURRENCY env var.", + ) + parser.add_argument( + "--verbosity", + "-v", + type=int, + default=1, + help="Verbosity level (0=minimal, 1=normal, 2=verbose). Default: 1", + ) + return parser.parse_args() + + +def main(): + """Start the background task worker.""" + logger = logging.getLogger(__name__) + args = parse_args() + + # Determine which queues to process + if args.queues: + queues = [q.strip() for q in args.queues.split(",")] + invalid = set(queues) - set(ALL_QUEUES) + if invalid: + sys.stderr.write(f"Error: Unknown queues: {', '.join(invalid)}\n") + sys.stderr.write(f"Valid queues are: {', '.join(ALL_QUEUES)}\n") + sys.exit(1) + else: + queues = DEFAULT_QUEUES.copy() + + # Apply exclusions + if args.exclude: + exclude = [q.strip() for q in args.exclude.split(",")] + invalid_exclude = set(exclude) - set(ALL_QUEUES) + if invalid_exclude: + sys.stderr.write( + f"Error: Unknown queues to exclude: {', '.join(invalid_exclude)}\n" + ) + sys.stderr.write(f"Valid queues are: {', '.join(ALL_QUEUES)}\n") + sys.exit(1) + queues = [q for q in queues if q not in exclude] + + if not queues: + sys.stderr.write("Error: No queues to process after exclusions.\n") + sys.exit(1) + + # Discover task modules + tasks_modules = discover_tasks_modules() + + # Build dramatiq CLI arguments and call main() directly. + # This avoids rundramatiq's os.execvp which replaces the process and + # discards our multiprocessing.set_start_method("fork") workaround. + dramatiq_args = [ + "dramatiq", + "--path", + ".", + "--processes", + str(args.concurrency or 4), + "--threads", + "1", + "--worker-shutdown-timeout", + "600000", + ] + + if args.verbosity > 1: + dramatiq_args.append("-v") + + dramatiq_args.extend(tasks_modules) + dramatiq_args.extend(["--queues", *queues]) + + logger.info("Starting worker with queues: %s", ", ".join(queues)) + + import dramatiq.cli # noqa: PLC0415 # pylint: disable=wrong-import-position + + sys.argv = dramatiq_args + dramatiq.cli.main() + + +if __name__ == "__main__": + main() diff --git a/docker/sabredav/Dockerfile b/src/caldav/Dockerfile similarity index 100% rename from docker/sabredav/Dockerfile rename to src/caldav/Dockerfile diff --git a/docker/sabredav/composer.json b/src/caldav/composer.json similarity index 85% rename from docker/sabredav/composer.json rename to src/caldav/composer.json index 94b6ad8..ec0bd64 100644 --- a/docker/sabredav/composer.json +++ b/src/caldav/composer.json @@ -10,7 +10,7 @@ ], "require": { "php": ">=8.1", - "sabre/dav": "dev-master", + "sabre/dav": "dev-master#1000fc028469c240fe13459e36648959f1519d09", "ext-pdo": "*", "ext-pdo_pgsql": "*" }, diff --git a/docker/sabredav/init-database.sh b/src/caldav/init-database.sh similarity index 97% rename from docker/sabredav/init-database.sh rename to src/caldav/init-database.sh index 9e87e73..fcc90af 100755 --- a/docker/sabredav/init-database.sh +++ b/src/caldav/init-database.sh @@ -47,7 +47,7 @@ SQL_DIR="${SQL_DIR:-/var/www/sabredav/sql}" TABLES_EXIST=$(psql -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('users', 'principals', 'calendars')" 2>/dev/null || echo "0") if [ "$TABLES_EXIST" -gt "0" ]; then - echo "sabre/dav tables already exist, skipping initialization" + echo "sabre/dav tables already exist, skipping table creation" exit 0 fi diff --git a/docker/sabredav/php-fpm.conf b/src/caldav/php-fpm.conf similarity index 96% rename from docker/sabredav/php-fpm.conf rename to src/caldav/php-fpm.conf index 1aaf26a..49a755c 100644 --- a/docker/sabredav/php-fpm.conf +++ b/src/caldav/php-fpm.conf @@ -5,7 +5,7 @@ pid = /tmp/php-fpm.pid [www] listen = /tmp/php-fpm.sock -listen.mode = 0666 +listen.mode = 0660 ; When running as non-root, user/group settings are ignored user = www-data diff --git a/docker/sabredav/sabredav.conf b/src/caldav/sabredav.conf similarity index 100% rename from docker/sabredav/sabredav.conf rename to src/caldav/sabredav.conf diff --git a/docker/sabredav/server.php b/src/caldav/server.php similarity index 80% rename from docker/sabredav/server.php rename to src/caldav/server.php index cf4bfa6..87657ba 100644 --- a/docker/sabredav/server.php +++ b/src/caldav/server.php @@ -14,7 +14,12 @@ use Calendars\SabreDav\HttpCallbackIMipPlugin; use Calendars\SabreDav\ApiKeyAuthBackend; use Calendars\SabreDav\CalendarSanitizerPlugin; use Calendars\SabreDav\AttendeeNormalizerPlugin; -use Calendars\SabreDav\ICSImportPlugin; +use Calendars\SabreDav\InternalApiPlugin; +use Calendars\SabreDav\ResourceAutoSchedulePlugin; +use Calendars\SabreDav\ResourceMkCalendarBlockPlugin; +use Calendars\SabreDav\CalendarsRoot; +use Calendars\SabreDav\CustomCalDAVPlugin; +use Calendars\SabreDav\PrincipalsRoot; // Composer autoloader require_once __DIR__ . '/vendor/autoload.php'; @@ -63,9 +68,11 @@ $carddavBackend = new CardDAV\Backend\PDO($pdo); $principalBackend = new AutoCreatePrincipalBackend($pdo); // Create directory tree +// Principal collections: principals/users/ and principals/resources/ +// Calendar collections: calendars/users/ and calendars/resources/ $nodes = [ - new CalDAV\Principal\Collection($principalBackend), - new CalDAV\CalendarRoot($principalBackend, $caldavBackend), + new PrincipalsRoot($principalBackend), + new CalendarsRoot($principalBackend, $caldavBackend), new CardDAV\AddressBookRoot($principalBackend, $carddavBackend), ]; @@ -73,9 +80,13 @@ $nodes = [ $server = new DAV\Server($nodes); $server->setBaseUri($baseUri); +// Give the principal backend a reference to the server +// so it can read X-CalDAV-Organization from the HTTP request +$principalBackend->setServer($server); + // Add plugins $server->addPlugin($authPlugin); -$server->addPlugin(new CalDAV\Plugin()); +$server->addPlugin(new CustomCalDAVPlugin()); $server->addPlugin(new CardDAV\Plugin()); $server->addPlugin(new DAVACL\Plugin()); $server->addPlugin(new DAV\Browser\Plugin()); @@ -137,9 +148,10 @@ $server->addPlugin(new CalendarSanitizerPlugin( // when processing calendar objects, fixing issues with REPLY handling $server->addPlugin(new AttendeeNormalizerPlugin()); -// Add ICS import plugin for bulk event import from a single POST request -// Only accessible via the X-Calendars-Import header (backend-only) -$server->addPlugin(new ICSImportPlugin($caldavBackend, $apiKey)); +// Add internal API plugin for resource provisioning and ICS import +// Gated by X-Internal-Api-Key header (separate from X-Api-Key used by proxy) +$internalApiKey = getenv('CALDAV_INTERNAL_API_KEY') ?: $apiKey; +$server->addPlugin(new InternalApiPlugin($pdo, $caldavBackend, $internalApiKey)); // Add custom IMipPlugin that forwards scheduling messages via HTTP callback // This MUST be added BEFORE the Schedule\Plugin so that Schedule\Plugin finds it @@ -164,7 +176,17 @@ $server->addPlugin($imipPlugin); $schedulePlugin = new CalDAV\Schedule\Plugin(); $server->addPlugin($schedulePlugin); -// error_log("[sabre/dav] Starting server"); +// Add resource auto-scheduling plugin +// Handles automatic accept/decline for resource principals based on availability +$server->addPlugin(new ResourceAutoSchedulePlugin($pdo, $caldavBackend)); + +// Block MKCALENDAR on resource principals (each resource has exactly one calendar) +$server->addPlugin(new ResourceMkCalendarBlockPlugin()); + +// Add property storage plugin for custom properties (resource metadata, etc.) +$server->addPlugin(new DAV\PropertyStorage\Plugin( + new DAV\PropertyStorage\Backend\PDO($pdo) +)); // Start server $server->start(); diff --git a/docker/sabredav/sql/pgsql.addressbooks.sql b/src/caldav/sql/pgsql.addressbooks.sql similarity index 97% rename from docker/sabredav/sql/pgsql.addressbooks.sql rename to src/caldav/sql/pgsql.addressbooks.sql index 98f414f..ffbcbc1 100644 --- a/docker/sabredav/sql/pgsql.addressbooks.sql +++ b/src/caldav/sql/pgsql.addressbooks.sql @@ -18,7 +18,7 @@ CREATE TABLE cards ( addressbookid INTEGER NOT NULL, carddata BYTEA, uri VARCHAR(200), - lastmodified INTEGER, + lastmodified BIGINT, etag VARCHAR(32), size INTEGER NOT NULL ); diff --git a/docker/sabredav/sql/pgsql.calendars.sql b/src/caldav/sql/pgsql.calendars.sql similarity index 85% rename from docker/sabredav/sql/pgsql.calendars.sql rename to src/caldav/sql/pgsql.calendars.sql index 90e5b54..01551ee 100644 --- a/docker/sabredav/sql/pgsql.calendars.sql +++ b/src/caldav/sql/pgsql.calendars.sql @@ -3,12 +3,12 @@ CREATE TABLE calendarobjects ( calendardata BYTEA, uri VARCHAR(200), calendarid INTEGER NOT NULL, - lastmodified INTEGER, + lastmodified BIGINT, etag VARCHAR(32), size INTEGER NOT NULL, componenttype VARCHAR(8), - firstoccurence INTEGER, - lastoccurence INTEGER, + firstoccurence BIGINT, + lastoccurence BIGINT, uid VARCHAR(200) ); @@ -32,17 +32,17 @@ ALTER TABLE ONLY calendars CREATE TABLE calendarinstances ( id SERIAL NOT NULL, calendarid INTEGER NOT NULL, - principaluri VARCHAR(100), + principaluri VARCHAR(255), access SMALLINT NOT NULL DEFAULT '1', -- '1 = owner, 2 = read, 3 = readwrite' - displayname VARCHAR(100), + displayname VARCHAR(255), uri VARCHAR(200), description TEXT, calendarorder INTEGER NOT NULL DEFAULT 0, calendarcolor VARCHAR(10), timezone TEXT, transparent SMALLINT NOT NULL DEFAULT '0', - share_href VARCHAR(100), - share_displayname VARCHAR(100), + share_href VARCHAR(255), + share_displayname VARCHAR(255), share_invitestatus SMALLINT NOT NULL DEFAULT '2' -- '1 = noresponse, 2 = accepted, 3 = declined, 4 = invalid' ); @@ -65,16 +65,16 @@ CREATE INDEX calendarinstances_principaluri_share_href CREATE TABLE calendarsubscriptions ( id SERIAL NOT NULL, uri VARCHAR(200) NOT NULL, - principaluri VARCHAR(100) NOT NULL, + principaluri VARCHAR(255) NOT NULL, source TEXT, - displayname VARCHAR(100), + displayname VARCHAR(255), refreshrate VARCHAR(10), calendarorder INTEGER NOT NULL DEFAULT 0, calendarcolor VARCHAR(10), striptodos SMALLINT NULL, stripalarms SMALLINT NULL, stripattachments SMALLINT NULL, - lastmodified INTEGER + lastmodified BIGINT ); ALTER TABLE ONLY calendarsubscriptions @@ -102,10 +102,16 @@ CREATE TABLE schedulingobjects ( principaluri VARCHAR(255), calendardata BYTEA, uri VARCHAR(200), - lastmodified INTEGER, + lastmodified BIGINT, etag VARCHAR(32), size INTEGER NOT NULL ); ALTER TABLE ONLY schedulingobjects ADD CONSTRAINT schedulingobjects_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX schedulingobjects_ukey + ON schedulingobjects USING btree (principaluri, uri); + +CREATE INDEX schedulingobjects_principaluri_ix + ON schedulingobjects USING btree (principaluri); diff --git a/docker/sabredav/sql/pgsql.locks.sql b/src/caldav/sql/pgsql.locks.sql similarity index 89% rename from docker/sabredav/sql/pgsql.locks.sql rename to src/caldav/sql/pgsql.locks.sql index 0290528..2753ee5 100644 --- a/docker/sabredav/sql/pgsql.locks.sql +++ b/src/caldav/sql/pgsql.locks.sql @@ -1,8 +1,8 @@ CREATE TABLE locks ( id SERIAL NOT NULL, owner VARCHAR(100), - timeout INTEGER, - created INTEGER, + timeout BIGINT, + created BIGINT, token VARCHAR(100), scope SMALLINT, depth SMALLINT, diff --git a/src/caldav/sql/pgsql.principals.sql b/src/caldav/sql/pgsql.principals.sql new file mode 100644 index 0000000..9e4bc10 --- /dev/null +++ b/src/caldav/sql/pgsql.principals.sql @@ -0,0 +1,40 @@ +CREATE TABLE principals ( + id SERIAL NOT NULL, + uri VARCHAR(200) NOT NULL, + email VARCHAR(255), + displayname VARCHAR(255), + calendar_user_type VARCHAR(20) DEFAULT 'INDIVIDUAL', + org_id VARCHAR(200) +); + +ALTER TABLE ONLY principals + ADD CONSTRAINT principals_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX principals_ukey + ON principals USING btree (uri); + +CREATE INDEX idx_principals_org_id + ON principals (org_id) + WHERE org_id IS NOT NULL; + +CREATE INDEX idx_principals_email + ON principals (email); + +CREATE INDEX idx_principals_cutype + ON principals (calendar_user_type) + WHERE calendar_user_type IN ('ROOM', 'RESOURCE'); + +CREATE TABLE groupmembers ( + id SERIAL NOT NULL, + principal_id INTEGER NOT NULL, + member_id INTEGER NOT NULL +); + +ALTER TABLE ONLY groupmembers + ADD CONSTRAINT groupmembers_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX groupmembers_ukey + ON groupmembers USING btree (principal_id, member_id); + +-- No seed data: principals are created via AutoCreatePrincipalBackend +-- (for users on first access) or InternalApiPlugin (for resources). diff --git a/docker/sabredav/sql/pgsql.propertystorage.sql b/src/caldav/sql/pgsql.propertystorage.sql similarity index 100% rename from docker/sabredav/sql/pgsql.propertystorage.sql rename to src/caldav/sql/pgsql.propertystorage.sql diff --git a/docker/sabredav/sql/pgsql.users.sql b/src/caldav/sql/pgsql.users.sql similarity index 100% rename from docker/sabredav/sql/pgsql.users.sql rename to src/caldav/sql/pgsql.users.sql diff --git a/docker/sabredav/src/ApiKeyAuthBackend.php b/src/caldav/src/ApiKeyAuthBackend.php similarity index 87% rename from docker/sabredav/src/ApiKeyAuthBackend.php rename to src/caldav/src/ApiKeyAuthBackend.php index 42f9174..af4c0f3 100644 --- a/docker/sabredav/src/ApiKeyAuthBackend.php +++ b/src/caldav/src/ApiKeyAuthBackend.php @@ -61,12 +61,17 @@ class ApiKeyAuthBackend implements BackendInterface } // Validate API key - if ($apiKeyHeader !== $this->apiKey) { + if (!hash_equals($this->apiKey, $apiKeyHeader)) { return [false, 'Invalid API key']; } + // Validate X-Forwarded-User to prevent path traversal + if (preg_match('/[\/\\\\]|\.\./', $xForwardedUser)) { + throw new \Sabre\DAV\Exception\NotAuthenticated('Invalid X-Forwarded-User header value'); + } + // Authentication successful - return [true, 'principals/' . $xForwardedUser]; + return [true, 'principals/users/' . $xForwardedUser]; } /** diff --git a/docker/sabredav/src/AttendeeNormalizerPlugin.php b/src/caldav/src/AttendeeNormalizerPlugin.php similarity index 100% rename from docker/sabredav/src/AttendeeNormalizerPlugin.php rename to src/caldav/src/AttendeeNormalizerPlugin.php diff --git a/src/caldav/src/AutoCreatePrincipalBackend.php b/src/caldav/src/AutoCreatePrincipalBackend.php new file mode 100644 index 0000000..dc8b223 --- /dev/null +++ b/src/caldav/src/AutoCreatePrincipalBackend.php @@ -0,0 +1,197 @@ + [ + 'dbField' => 'displayname', + ], + '{http://sabredav.org/ns}email-address' => [ + 'dbField' => 'email', + ], + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => [ + 'dbField' => 'calendar_user_type', + ], + ]; + + /** + * @var \Sabre\DAV\Server|null + */ + private $server = null; + + /** + * Set the server reference (called from server.php after server creation). + * + * @param \Sabre\DAV\Server $server + */ + public function setServer(\Sabre\DAV\Server $server) + { + $this->server = $server; + } + + /** + * Get the org_id from the current HTTP request's X-CalDAV-Organization header. + * + * @return string|null + */ + private function getRequestOrgId() + { + if ($this->server && $this->server->httpRequest) { + return $this->server->httpRequest->getHeader('X-CalDAV-Organization'); + } + return null; + } + + /** + * Returns a specific principal, specified by its path. + * Auto-creates the principal if it doesn't exist. + * + * NOT org-filtered: allows cross-org sharing and scheduling. + * + * @param string $path + * @return array|null + */ + public function getPrincipalByPath($path) + { + $principal = parent::getPrincipalByPath($path); + + // If principal doesn't exist, create it automatically + // Only auto-create user principals (principals/users/*). + // Resource principals (principals/resources/*) are provisioned via Django. + if (!$principal && strpos($path, 'principals/users/') === 0) { + // Extract username from path + $username = substr($path, strlen('principals/users/')); + + $pdo = $this->pdo; + $tableName = $this->tableName; + $orgId = $this->getRequestOrgId(); + + try { + $stmt = $pdo->prepare( + 'INSERT INTO ' . $tableName + . ' (uri, email, displayname, calendar_user_type, org_id)' + . ' VALUES (?, ?, ?, ?, ?)' + . ' ON CONFLICT (uri) DO UPDATE SET org_id = COALESCE(EXCLUDED.org_id, ' + . $tableName . '.org_id)' + ); + $stmt->execute([$path, $username, $username, 'INDIVIDUAL', $orgId]); + + // Retry getting the principal + $principal = parent::getPrincipalByPath($path); + } catch (\Exception $e) { + error_log("Failed to auto-create principal: " . $e->getMessage()); + return null; + } + } + + return $principal; + } + + /** + * Returns a list of principals based on a prefix. + * + * Org-filtered: only returns principals from the requesting user's org. + * + * @param string $prefixPath + * @return array + */ + public function getPrincipalsByPrefix($prefixPath) + { + $principals = parent::getPrincipalsByPrefix($prefixPath); + + $orgId = $this->getRequestOrgId(); + if (!$orgId) { + return $principals; + } + + // Filter by org_id + $filteredUris = $this->getOrgPrincipalUris($prefixPath, $orgId); + if ($filteredUris === null) { + return $principals; + } + + return array_values(array_filter($principals, function ($principal) use ($filteredUris) { + return in_array($principal['uri'], $filteredUris, true); + })); + } + + /** + * Search principals matching certain criteria. + * + * Org-filtered: only returns principals from the requesting user's org. + * + * @param string $prefixPath + * @param array $searchProperties + * @param string $test + * @return array + */ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + $results = parent::searchPrincipals($prefixPath, $searchProperties, $test); + + $orgId = $this->getRequestOrgId(); + if (!$orgId) { + return $results; + } + + $filteredUris = $this->getOrgPrincipalUris($prefixPath, $orgId); + if ($filteredUris === null) { + return $results; + } + + return array_values(array_filter($results, function ($uri) use ($filteredUris) { + return in_array($uri, $filteredUris, true); + })); + } + + /** + * Get principal URIs for a given prefix and org_id. + * + * @param string $prefixPath + * @param string $orgId + * @return array|null + */ + private function getOrgPrincipalUris($prefixPath, $orgId) + { + try { + $stmt = $this->pdo->prepare( + 'SELECT uri FROM ' . $this->tableName + . ' WHERE uri LIKE ? AND org_id = ?' + ); + $stmt->execute([$prefixPath . '/%', $orgId]); + return $stmt->fetchAll(\PDO::FETCH_COLUMN, 0); + } catch (\Exception $e) { + error_log("Failed to query org principals: " . $e->getMessage()); + return null; + } + } +} diff --git a/docker/sabredav/src/CalendarSanitizerPlugin.php b/src/caldav/src/CalendarSanitizerPlugin.php similarity index 98% rename from docker/sabredav/src/CalendarSanitizerPlugin.php rename to src/caldav/src/CalendarSanitizerPlugin.php index db83527..e6bba08 100644 --- a/docker/sabredav/src/CalendarSanitizerPlugin.php +++ b/src/caldav/src/CalendarSanitizerPlugin.php @@ -143,7 +143,7 @@ class CalendarSanitizerPlugin extends ServerPlugin * Sanitize a parsed VCalendar object in-place. * Strips binary attachments and truncates oversized descriptions. * - * Also called by ICSImportPlugin for direct DB writes that bypass + * Also called by InternalApiPlugin for direct DB writes that bypass * the HTTP layer (and thus don't trigger beforeCreateFile hooks). * * @return bool True if the VCalendar was modified. @@ -206,7 +206,7 @@ class CalendarSanitizerPlugin extends ServerPlugin /** * Check that a VCalendar's serialized size is within the max resource limit. - * Called by ICSImportPlugin for the direct DB write path. + * Called by InternalApiPlugin for the direct DB write path. * * @throws InsufficientStorage if the serialized size exceeds the limit. */ diff --git a/src/caldav/src/CalendarsRoot.php b/src/caldav/src/CalendarsRoot.php new file mode 100644 index 0000000..d17bc21 --- /dev/null +++ b/src/caldav/src/CalendarsRoot.php @@ -0,0 +1,86 @@ +children = [ + new NamedCalendarRoot('users', $principalBackend, $caldavBackend, 'principals/users'), + new NamedCalendarRoot('resources', $principalBackend, $caldavBackend, 'principals/resources'), + ]; + } + + public function getName() + { + return 'calendars'; + } + + public function getChild($name) + { + foreach ($this->children as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new DAV\Exception\NotFound('Collection ' . $name . ' not found'); + } + + public function getChildren() + { + return $this->children; + } +} + +/** + * A CalendarRoot whose getName() returns a custom value instead of 'calendars'. + * + * Used as a child of CalendarsRoot so that: + * calendars/users/ → NamedCalendarRoot('users', ..., 'principals/users') + * calendars/resources/ → NamedCalendarRoot('resources', ..., 'principals/resources') + */ +class NamedCalendarRoot extends CalDAV\CalendarRoot +{ + /** @var string */ + private $nodeName; + + public function __construct( + string $nodeName, + PrincipalBackendInterface $principalBackend, + CalDAVBackendInterface $caldavBackend, + string $principalPrefix + ) { + parent::__construct($principalBackend, $caldavBackend, $principalPrefix); + $this->nodeName = $nodeName; + } + + public function getName() + { + return $this->nodeName; + } +} diff --git a/src/caldav/src/CustomCalDAVPlugin.php b/src/caldav/src/CustomCalDAVPlugin.php new file mode 100644 index 0000000..f65f003 --- /dev/null +++ b/src/caldav/src/CustomCalDAVPlugin.php @@ -0,0 +1,52 @@ +pdo = $pdo; + $this->caldavBackend = $caldavBackend; + $this->apiKey = $apiKey; + } + + public function getPluginName() + { + return 'internal-api'; + } + + public function initialize(Server $server) + { + $this->server = $server; + // Use method:* (not beforeMethod:*) so SabreDAV calls sendResponse() + // for us after the handler returns false. + $server->on('method:*', [$this, 'handleRequest'], 90); + } + + /** + * Intercept all requests under /internal-api/. + * + * @return bool|null false to stop event propagation, null to let + * other handlers proceed. + */ + public function handleRequest($request, $response) + { + $path = $request->getPath(); + + // Only handle /internal-api/ routes + if (strpos($path, 'internal-api/') !== 0 && $path !== 'internal-api') { + return; + } + + // Verify the dedicated internal API key header + $headerValue = $request->getHeader('X-Internal-Api-Key'); + if (!$headerValue || !hash_equals($this->apiKey, $headerValue)) { + $response->setStatus(403); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => 'Forbidden: missing or invalid X-Internal-Api-Key header', + ])); + return false; + } + + $method = $request->getMethod(); + + // Route: POST /internal-api/resources/ + if ($method === 'POST' && preg_match('#^internal-api/resources/?$#', $path)) { + $this->handleCreateResource($request, $response); + return false; + } + + // Route: DELETE /internal-api/resources/{resource_id} + if ($method === 'DELETE' && preg_match('#^internal-api/resources/([a-zA-Z0-9-]+)$#', $path, $matches)) { + $this->handleDeleteResource($request, $response, $matches[1]); + return false; + } + + // Route: POST /internal-api/users/delete + if ($method === 'POST' && preg_match('#^internal-api/users/delete/?$#', $path)) { + $body = json_decode($request->getBodyAsString(), true); + $email = $body['email'] ?? null; + if (!$email) { + $response->setStatus(400); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['error' => 'email is required'])); + return false; + } + $this->handleDeleteUser($request, $response, $email); + return false; + } + + // Route: POST /internal-api/import/{principalUser}/{calendarUri} + if ($method === 'POST' && preg_match('#^internal-api/import/([^/]+)/([^/]+)$#', $path, $matches)) { + $this->handleImport($request, $response, urldecode($matches[1]), $matches[2]); + return false; + } + + $response->setStatus(404); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => 'Not found', + ])); + return false; + } + + /** + * POST /internal-api/resources/ + * Create a resource principal and its default calendar. + */ + private function handleCreateResource($request, $response) + { + $body = json_decode($request->getBodyAsString(), true); + if (!$body) { + $response->setStatus(400); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['error' => 'Invalid JSON body'])); + return false; + } + + $resourceId = $body['resource_id'] ?? null; + $name = $body['name'] ?? null; + $email = $body['email'] ?? null; + $resourceType = $body['resource_type'] ?? 'ROOM'; + $orgId = $body['org_id'] ?? null; + + if (!$resourceId || !$name || !$email) { + $response->setStatus(400); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => 'Missing required fields: resource_id, name, email', + ])); + return false; + } + + $principalUri = 'principals/resources/' . $resourceId; + + // Wrap principal + calendar creation in a transaction for atomicity + $this->pdo->beginTransaction(); + try { + // Insert principal with ON CONFLICT DO NOTHING + $stmt = $this->pdo->prepare( + 'INSERT INTO principals (uri, email, displayname, calendar_user_type, org_id)' + . ' VALUES (?, ?, ?, ?, ?)' + . ' ON CONFLICT (uri) DO NOTHING' + ); + $stmt->execute([$principalUri, $email, $name, $resourceType, $orgId]); + + if ($stmt->rowCount() === 0) { + $this->pdo->rollBack(); + $response->setStatus(409); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => "Resource '$resourceId' already exists", + ])); + return false; + } + + // Create default calendar + $calendarUri = 'default'; + $this->caldavBackend->createCalendar( + $principalUri, + $calendarUri, + [ + '{DAV:}displayname' => $name, + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' + => new \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet(['VEVENT']), + ] + ); + + $this->pdo->commit(); + } catch (\Exception $e) { + $this->pdo->rollBack(); + error_log("[InternalApiPlugin] Failed to create resource: " . $e->getMessage()); + $response->setStatus(500); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => 'Failed to create resource', + ])); + return false; + } + + $response->setStatus(201); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'principal_uri' => $principalUri, + 'email' => $email, + ])); + return false; + } + + /** + * DELETE /internal-api/resources/{resource_id} + * Delete a resource principal, its calendars, and all associated data. + */ + private function handleDeleteResource($request, $response, $resourceId) + { + $principalUri = 'principals/resources/' . $resourceId; + $orgId = $request->getHeader('X-CalDAV-Organization'); + + // Look up the principal + try { + $stmt = $this->pdo->prepare( + 'SELECT email, org_id FROM principals WHERE uri = ?' + ); + $stmt->execute([$principalUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + } catch (\Exception $e) { + error_log("[InternalApiPlugin] Failed to look up principal: " . $e->getMessage()); + $response->setStatus(500); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['error' => 'Failed to look up resource'])); + return false; + } + + if (!$row) { + $response->setStatus(404); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => "Resource '$resourceId' not found", + ])); + return false; + } + + // Verify org scoping — reject if orgs don't match or either is missing + if (!$orgId || !$row['org_id'] || $orgId !== $row['org_id']) { + $response->setStatus(403); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => 'Cannot delete a resource from a different organization', + ])); + return false; + } + + // Delete calendars and their objects + try { + $calendars = $this->caldavBackend->getCalendarsForUser($principalUri); + foreach ($calendars as $calendar) { + $this->caldavBackend->deleteCalendar($calendar['id']); + } + } catch (\Exception $e) { + error_log("[InternalApiPlugin] Failed to delete calendars: " . $e->getMessage()); + } + + // Delete scheduling objects, principal rows + $this->deletePrincipalRows($principalUri); + + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['deleted' => true])); + return false; + } + + /** + * Delete principal row and associated proxy/scheduling rows. + */ + private function deletePrincipalRows($principalUri) + { + try { + // Delete scheduling objects if the table exists + $stmt = $this->pdo->prepare( + "SELECT EXISTS (" + . " SELECT FROM information_schema.tables" + . " WHERE table_name = 'schedulingobjects'" + . ")" + ); + $stmt->execute(); + if ($stmt->fetchColumn()) { + $del = $this->pdo->prepare( + 'DELETE FROM schedulingobjects WHERE principaluri = ?' + ); + $del->execute([$principalUri]); + } + + // Delete principal and proxy rows + $del = $this->pdo->prepare('DELETE FROM principals WHERE uri = ?'); + $del->execute([$principalUri]); + + $del = $this->pdo->prepare('DELETE FROM principals WHERE uri LIKE ?'); + $del->execute([$principalUri . '/%']); + } catch (\Exception $e) { + error_log("[InternalApiPlugin] Failed to delete principal rows: " . $e->getMessage()); + } + } + + /** + * POST /internal-api/users/delete + * Delete a user principal and all their calendar data. + * Body: {"email": "user@example.com"} + */ + private function handleDeleteUser($request, $response, $email) + { + $principalUri = 'principals/users/' . $email; + $orgId = $request->getHeader('X-CalDAV-Organization'); + + // Look up the principal + try { + $stmt = $this->pdo->prepare( + 'SELECT id, org_id FROM principals WHERE uri = ?' + ); + $stmt->execute([$principalUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + } catch (\Exception $e) { + error_log("[InternalApiPlugin] Failed to look up user principal: " . $e->getMessage()); + $response->setStatus(500); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['error' => 'Failed to look up user'])); + return false; + } + + if (!$row) { + // Principal doesn't exist — nothing to clean up + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['deleted' => true, 'existed' => false])); + return false; + } + + // Verify org scoping — reject if orgs don't match or either is missing + if ($row['org_id']) { + if (!$orgId || $orgId !== $row['org_id']) { + $response->setStatus(403); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'error' => 'Cannot delete a user from a different organization', + ])); + return false; + } + } + + // Delete calendars and their objects + try { + $calendars = $this->caldavBackend->getCalendarsForUser($principalUri); + foreach ($calendars as $calendar) { + $this->caldavBackend->deleteCalendar($calendar['id']); + } + } catch (\Exception $e) { + error_log("[InternalApiPlugin] Failed to delete user calendars: " . $e->getMessage()); + } + + // Delete scheduling objects, principal rows + $this->deletePrincipalRows($principalUri); + + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['deleted' => true, 'existed' => true])); + return false; + } + + /** + * POST /internal-api/import/{principalUser}/{calendarUri} + * Bulk import events from a multi-event ICS file. + */ + private function handleImport($request, $response, $principalUser, $calendarUri) + { + $principalUri = 'principals/users/' . $principalUser; + + // Look up calendarId + $calendarId = $this->resolveCalendarId($principalUri, $calendarUri); + if ($calendarId === null) { + $response->setStatus(404); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['error' => 'Calendar not found'])); + return false; + } + + // Read and parse the raw ICS body + $icsBody = $request->getBodyAsString(); + if (empty($icsBody)) { + $response->setStatus(400); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['error' => 'Empty request body'])); + return false; + } + + try { + $vcal = VObject\Reader::read($icsBody); + } catch (\Exception $e) { + error_log("[InternalApiPlugin] Failed to parse ICS: " . $e->getMessage()); + $response->setStatus(400); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode(['error' => 'Failed to parse ICS file'])); + return false; + } + + // Validate and auto-repair (fixes missing VALARM ACTION, etc.) + $vcal->validate(VObject\Component::REPAIR); + + // Split by UID using the stream-based splitter + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $vcal->serialize()); + rewind($stream); + + $splitter = new VObject\Splitter\ICalendar($stream); + + $totalEvents = 0; + $importedCount = 0; + $duplicateCount = 0; + $skippedCount = 0; + $errors = []; + + try { + while ($splitVcal = $splitter->getNext()) { + $totalEvents++; + + try { + // Extract UID from the first VEVENT + $uid = null; + foreach ($splitVcal->VEVENT as $vevent) { + if (isset($vevent->UID)) { + $uid = (string)$vevent->UID; + break; + } + } + + if (!$uid) { + $uid = \Sabre\DAV\UUIDUtil::getUUID(); + } + + // Sanitize event data (strip attachments, truncate descriptions) + $this->sanitizeAndCheckSize($splitVcal); + + $objectUri = $uid . '.ics'; + $data = $splitVcal->serialize(); + + $this->caldavBackend->createCalendarObject( + $calendarId, + $objectUri, + $data + ); + $importedCount++; + } catch (\Exception $e) { + $msg = $e->getMessage(); + $summary = ''; + if (isset($splitVcal->VEVENT) && isset($splitVcal->VEVENT->SUMMARY)) { + $summary = (string)$splitVcal->VEVENT->SUMMARY; + } + + if (strpos($msg, '23505') !== false) { + $duplicateCount++; + } elseif (strpos($msg, 'valid instances') !== false) { + $skippedCount++; + } else { + $skippedCount++; + if (count($errors) < 10) { + $errors[] = [ + 'uid' => $uid ?? 'unknown', + 'summary' => $summary, + 'error' => $msg, + ]; + } + error_log( + "[InternalApiPlugin] Failed to import event " + . "uid=" . ($uid ?? 'unknown') + . " summary={$summary}: {$msg}" + ); + } + } + } + } finally { + fclose($stream); + } + + error_log( + "[InternalApiPlugin] Import complete: " + . "{$importedCount} imported, " + . "{$duplicateCount} duplicates, " + . "{$skippedCount} failed " + . "out of {$totalEvents} total" + ); + + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/json'); + $response->setBody(json_encode([ + 'total_events' => $totalEvents, + 'imported_count' => $importedCount, + 'duplicate_count' => $duplicateCount, + 'skipped_count' => $skippedCount, + 'errors' => $errors, + ])); + + return false; + } + + /** + * Sanitize a split VCALENDAR before import and enforce max resource size. + */ + private function sanitizeAndCheckSize(VObject\Component\VCalendar $vcal) + { + $sanitizer = $this->server->getPlugin('calendar-sanitizer'); + if ($sanitizer) { + $sanitizer->sanitizeVCalendar($vcal); + $sanitizer->checkResourceSize($vcal); + } + } + + /** + * Resolve the internal calendar ID from a principal URI and calendar URI. + * + * @param string $principalUri e.g. "principals/users/user@example.com" + * @param string $calendarUri e.g. "a1b2c3d4-..." + * @return array|null The calendarId pair, or null if not found. + */ + private function resolveCalendarId(string $principalUri, string $calendarUri) + { + $calendars = $this->caldavBackend->getCalendarsForUser($principalUri); + + foreach ($calendars as $calendar) { + if ($calendar['uri'] === $calendarUri) { + return $calendar['id']; + } + } + + return null; + } + + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Internal API for resource provisioning and ICS import', + ]; + } +} diff --git a/src/caldav/src/PrincipalsRoot.php b/src/caldav/src/PrincipalsRoot.php new file mode 100644 index 0000000..ea36fe3 --- /dev/null +++ b/src/caldav/src/PrincipalsRoot.php @@ -0,0 +1,119 @@ +children = [ + new NamedPrincipalCollection('users', $principalBackend, 'principals/users'), + new ResourcePrincipalCollection('resources', $principalBackend, 'principals/resources'), + ]; + } + + public function getName() + { + return 'principals'; + } + + public function getChild($name) + { + foreach ($this->children as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new DAV\Exception\NotFound('Collection ' . $name . ' not found'); + } + + public function getChildren() + { + return $this->children; + } +} + +/** + * A Principal\Collection whose getName() returns a custom value. + * + * Used as a child of PrincipalsRoot so that: + * principals/users/ → NamedPrincipalCollection('users', ..., 'principals/users') + * principals/resources/ → NamedPrincipalCollection('resources', ..., 'principals/resources') + */ +class NamedPrincipalCollection extends CalDAV\Principal\Collection +{ + /** @var string */ + private $nodeName; + + public function __construct( + string $nodeName, + PrincipalBackendInterface $principalBackend, + string $principalPrefix + ) { + parent::__construct($principalBackend, $principalPrefix); + $this->nodeName = $nodeName; + } + + public function getName() + { + return $this->nodeName; + } +} + +/** + * Principal collection for resources that returns ResourcePrincipal nodes. + * + * Resource principals have no DAV owner, so the default ACL (which only + * grants {DAV:}all to {DAV:}owner) blocks all property reads with 403. + * This collection returns ResourcePrincipal nodes that additionally grant + * {DAV:}read to {DAV:}authenticated, allowing any logged-in user to + * discover resource names, types, and emails via PROPFIND. + */ +class ResourcePrincipalCollection extends NamedPrincipalCollection +{ + public function getChildForPrincipal(array $principal) + { + return new ResourcePrincipal($this->principalBackend, $principal); + } +} + +/** + * A principal node with a permissive read ACL for resource discovery. + */ +class ResourcePrincipal extends CalDAV\Principal\User +{ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + } +} diff --git a/src/caldav/src/ResourceAutoSchedulePlugin.php b/src/caldav/src/ResourceAutoSchedulePlugin.php new file mode 100644 index 0000000..128c822 --- /dev/null +++ b/src/caldav/src/ResourceAutoSchedulePlugin.php @@ -0,0 +1,385 @@ + 110). + * + * This plugin also sets $message->scheduleStatus before HttpCallbackIMipPlugin + * runs, which prevents email delivery to resource addresses (resource addresses + * are not real mailboxes). + */ + +namespace Calendars\SabreDav; + +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\VObject\ITip\Message; +use Sabre\VObject\Reader; +use Sabre\CalDAV\Backend\PDO as CalDAVBackend; + +class ResourceAutoSchedulePlugin extends ServerPlugin +{ + /** @var Server */ + protected $server; + + /** @var \PDO */ + private $pdo; + + /** @var CalDAVBackend */ + private $caldavBackend; + + /** Custom namespace for resource properties */ + private const NS = 'urn:lasuite:calendars'; + + public function __construct(\PDO $pdo, CalDAVBackend $caldavBackend) + { + $this->pdo = $pdo; + $this->caldavBackend = $caldavBackend; + } + + public function getPluginName() + { + return 'resource-auto-schedule'; + } + + public function initialize(Server $server) + { + $this->server = $server; + // Priority 120: runs after Schedule\Plugin (110) + $server->on('schedule', [$this, 'autoSchedule'], 120); + // Priority 200: runs BEFORE Schedule\Plugin's propFindEarly (150) + // which hardcodes calendar-user-type to 'INDIVIDUAL'. By setting + // the real value first, the Schedule\Plugin's handle() becomes a no-op. + $server->on('propFind', [$this, 'propFindResourceType'], 200); + } + + /** + * Set the correct calendar-user-type for resource principals. + * + * Schedule\Plugin::propFindEarly (priority 150) hardcodes INDIVIDUAL via + * handle(), which only fires when the property isn't already resolved. + * By setting the real DB value here at priority 200 via set(), we pre-empt it. + */ + public function propFindResourceType(\Sabre\DAV\PropFind $propFind, \Sabre\DAV\INode $node) + { + if (!($node instanceof ResourcePrincipal)) { + return; + } + + $props = $node->getProperties( + ['{urn:ietf:params:xml:ns:caldav}calendar-user-type'] + ); + $cutype = $props['{urn:ietf:params:xml:ns:caldav}calendar-user-type'] ?? null; + if ($cutype) { + $propFind->set('{urn:ietf:params:xml:ns:caldav}calendar-user-type', $cutype); + } + } + + /** + * Handle scheduling messages to resource principals. + * + * @param Message $message + */ + public function autoSchedule(Message $message) + { + // Only handle REQUEST method (new invitations and updates) + if ($message->method !== 'REQUEST') { + return; + } + + // Only handle messages to resource principals + $recipientPrincipal = $this->resolveRecipientPrincipal($message->recipient); + if (!$recipientPrincipal) { + return; + } + + $cutype = $recipientPrincipal['calendar_user_type'] ?? 'INDIVIDUAL'; + if (!in_array($cutype, ['ROOM', 'RESOURCE'], true)) { + return; + } + + // Enforce org scoping: reject cross-org bookings + $requestOrgId = $this->server->httpRequest + ? $this->server->httpRequest->getHeader('X-CalDAV-Organization') + : null; + $resourceOrgId = $recipientPrincipal['org_id'] ?? null; + + if ($resourceOrgId) { + if (!$requestOrgId || $requestOrgId !== $resourceOrgId) { + $this->declineInvitation($message, 'Cross-organization booking not allowed'); + return; + } + } + + // Read auto-schedule mode from propertystorage + $mode = $this->getAutoScheduleMode($recipientPrincipal['uri']); + + switch ($mode) { + case 'accept-always': + $this->acceptInvitation($message); + break; + + case 'decline-always': + $this->declineInvitation($message, 'Resource is offline'); + break; + + case 'manual': + // Leave as NEEDS-ACTION for manual approval + // But still set scheduleStatus to prevent email delivery + $message->scheduleStatus = '1.0;Pending manual approval'; + break; + + case 'automatic': + default: + if ($this->hasConflict($recipientPrincipal, $message)) { + $this->declineInvitation($message, 'Resource is busy'); + } else { + $this->acceptInvitation($message); + } + break; + } + } + + /** + * Resolve the recipient email to a principal record. + * + * @param string $recipient mailto: URI + * @return array|null Principal row or null + */ + private function resolveRecipientPrincipal($recipient) + { + $email = $this->extractEmail($recipient); + if (!$email) { + return null; + } + + try { + $stmt = $this->pdo->prepare( + 'SELECT id, uri, email, calendar_user_type, org_id' + . ' FROM principals WHERE email = ?' + ); + $stmt->execute([strtolower($email)]); + return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null; + } catch (\Exception $e) { + error_log("[ResourceAutoSchedulePlugin] DB error: " . $e->getMessage()); + return null; + } + } + + /** + * Extract email from a mailto: URI. + * + * @param string $uri + * @return string|null + */ + private function extractEmail($uri) + { + if (stripos($uri, 'mailto:') === 0) { + return strtolower(substr($uri, 7)); + } + return null; + } + + /** + * Get auto-schedule mode from propertystorage. + * + * @param string $principalUri + * @return string + */ + private function getAutoScheduleMode($principalUri) + { + try { + $stmt = $this->pdo->prepare( + "SELECT value FROM propertystorage" + . " WHERE path = ? AND name = '{" . self::NS . "}auto-schedule-mode'" + ); + $stmt->execute([$principalUri]); + $result = $stmt->fetchColumn(); + return $result ?: 'automatic'; + } catch (\Exception $e) { + error_log("[ResourceAutoSchedulePlugin] Failed to read auto-schedule mode: " . $e->getMessage()); + return 'automatic'; + } + } + + /** + * Check if the resource has a conflict with the incoming event. + * + * @param array $principal + * @param Message $message + * @return bool + */ + private function hasConflict($principal, Message $message) + { + if (!$message->message) { + return false; + } + + $vcalendar = $message->message; + + // Get the resource's calendar + $calendarId = $this->getResourceCalendarId($principal['uri']); + if (!$calendarId) { + return false; // No calendar = no conflicts + } + + // Extract time ranges from all VEVENT components + foreach ($vcalendar->VEVENT as $vevent) { + // Skip transparent events + $transp = isset($vevent->TRANSP) ? (string)$vevent->TRANSP : 'OPAQUE'; + if ($transp === 'TRANSPARENT') { + continue; + } + + $dtstart = $vevent->DTSTART ? $vevent->DTSTART->getDateTime() : null; + $dtend = null; + + if (isset($vevent->DTEND)) { + $dtend = $vevent->DTEND->getDateTime(); + } elseif (isset($vevent->DURATION)) { + $dtend = clone $dtstart; + $dtend->add($vevent->DURATION->getDateInterval()); + } + + if (!$dtstart || !$dtend) { + continue; + } + + // Query for overlapping events in the resource's calendar + $startTs = $dtstart->getTimestamp(); + $endTs = $dtend->getTimestamp(); + + // Get UID of the incoming event to exclude updates to the same event + $uid = isset($vevent->UID) ? (string)$vevent->UID : null; + + if ($this->hasOverlappingEvents($calendarId, $startTs, $endTs, $uid)) { + return true; + } + } + + return false; + } + + /** + * Get the resource's default calendar ID. + * + * @param string $principalUri + * @return array|null [calendarId, instanceId] pair or null + */ + private function getResourceCalendarId($principalUri) + { + $calendars = $this->caldavBackend->getCalendarsForUser($principalUri); + if (!empty($calendars)) { + return $calendars[0]['id']; + } + return null; + } + + /** + * Check for overlapping events in a calendar. + * + * @param array $calendarId [calendarId, instanceId] + * @param int $startTs Start timestamp + * @param int $endTs End timestamp + * @param string|null $excludeUid UID to exclude (for updates) + * @return bool + */ + private function hasOverlappingEvents($calendarId, $startTs, $endTs, $excludeUid = null) + { + try { + // Normalize calendarId: SabreDAV may return an array [id, instanceId] + // or a scalar integer depending on the version/backend. + $calId = is_array($calendarId) ? $calendarId[0] : $calendarId; + + // Use calendarobjects table directly for conflict check + // firstoccurence and lastoccurence are Unix timestamps stored by SabreDAV + $sql = 'SELECT COUNT(*) FROM calendarobjects' + . ' WHERE calendarid = ?' + . ' AND firstoccurence < ? AND lastoccurence > ?'; + $params = [$calId, $endTs, $startTs]; + + if ($excludeUid) { + $sql .= ' AND uid != ?'; + $params[] = $excludeUid; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return (int)$stmt->fetchColumn() > 0; + } catch (\Exception $e) { + error_log("[ResourceAutoSchedulePlugin] Conflict check failed: " . $e->getMessage()); + return true; // Fail-closed: reject booking if check fails + } + } + + /** + * Accept the invitation. + * + * @param Message $message + */ + private function acceptInvitation(Message $message) + { + $message->scheduleStatus = '1.2;Scheduling message delivered (auto-accepted)'; + + // Update PARTSTAT in the delivered calendar object + $this->updatePartstat($message, 'ACCEPTED'); + } + + /** + * Decline the invitation. + * + * @param Message $message + * @param string $reason + */ + private function declineInvitation(Message $message, $reason = '') + { + $message->scheduleStatus = '3.0;Scheduling message declined' . ($reason ? ": $reason" : ''); + + // Update PARTSTAT in the delivered calendar object + $this->updatePartstat($message, 'DECLINED'); + } + + /** + * Update the PARTSTAT of the resource attendee in the iTIP message. + * + * @param Message $message + * @param string $partstat ACCEPTED, DECLINED, etc. + */ + private function updatePartstat(Message $message, $partstat) + { + if (!$message->message) { + return; + } + + $recipientEmail = $this->extractEmail($message->recipient); + if (!$recipientEmail) { + return; + } + + foreach ($message->message->VEVENT as $vevent) { + if (!isset($vevent->ATTENDEE)) { + continue; + } + foreach ($vevent->ATTENDEE as $attendee) { + $email = $this->extractEmail((string)$attendee); + if ($email === $recipientEmail) { + $attendee['PARTSTAT'] = $partstat; + } + } + } + } + + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Auto-scheduling for resource principals (rooms, equipment)', + ]; + } +} diff --git a/src/caldav/src/ResourceMkCalendarBlockPlugin.php b/src/caldav/src/ResourceMkCalendarBlockPlugin.php new file mode 100644 index 0000000..044e6e1 --- /dev/null +++ b/src/caldav/src/ResourceMkCalendarBlockPlugin.php @@ -0,0 +1,63 @@ +server = $server; + // Hook before MKCALENDAR is processed + $server->on('beforeMethod:MKCALENDAR', [$this, 'beforeMkCalendar'], 90); + } + + /** + * Block MKCALENDAR on resource principal calendar homes. + * + * @param \Sabre\HTTP\RequestInterface $request + * @param \Sabre\HTTP\ResponseInterface $response + * @return bool|null false to stop, null to continue + */ + public function beforeMkCalendar($request, $response) + { + $path = $request->getPath(); + + // Check if the path is under a resource calendar home + // Resource calendar homes: calendars/resources/{id}/ + if (preg_match('#^calendars/resources/#', $path)) { + throw new Forbidden( + 'Resource principals can only have one calendar. ' + . 'Additional calendar creation is not allowed.' + ); + } + + return null; // Allow for non-resource paths + } + + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Blocks MKCALENDAR on resource principal calendar homes', + ]; + } +} diff --git a/src/frontend/Caddyfile b/src/frontend/Caddyfile new file mode 100644 index 0000000..a44dd69 --- /dev/null +++ b/src/frontend/Caddyfile @@ -0,0 +1,19 @@ +{ + auto_https off + admin off +} + +:8080 { + root * /srv + header X-Frame-Options DENY + + route { + try_files {path} /index.html + file_server + } + + handle_errors { + rewrite * /404.html + file_server + } +} diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index f589e04..8bc1338 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -1,13 +1,12 @@ -FROM node:22-alpine AS frontend-deps +FROM node:24-alpine AS frontend-deps WORKDIR /home/frontend/ COPY ./package.json ./package.json -COPY ./yarn.lock ./yarn.lock +COPY ./package-lock.json ./package-lock.json COPY ./apps/calendars/package.json ./apps/calendars/package.json - -RUN yarn install --frozen-lockfile +RUN npm ci COPY .dockerignore ./.dockerignore # COPY ./.prettierrc.js ./.prettierrc.js @@ -26,16 +25,16 @@ WORKDIR /home/frontend/apps/calendars FROM frontend-deps AS calendars-dev -WORKDIR /home/frontend/apps/calendars +WORKDIR /home/frontend EXPOSE 3000 -RUN yarn build-theme +RUN cd apps/calendars && npm run build-theme # Build open-calendar package if dist doesn't exist, then start dev server -CMD ["/bin/sh", "-c", "cd /home/frontend/apps/calendars && yarn dev"] +CMD ["/bin/sh", "-c", "cd /home/frontend/apps/calendars && npm run dev"] -# Tilt will rebuild calendars target so, we dissociate calendars and calendars-builder +# Tilt will rebuild calendars target so, we dissociate calendars and calendars-builder # to avoid rebuilding the app at every changes. FROM calendars AS calendars-builder @@ -44,28 +43,20 @@ WORKDIR /home/frontend/apps/calendars ARG API_ORIGIN ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN} -RUN yarn build +RUN npm run build # ---- Front-end image ---- -FROM nginxinc/nginx-unprivileged:alpine3.22 AS frontend-production +FROM caddy:2-alpine AS frontend-production # Upgrade system packages to install security updates -USER root RUN apk update && \ apk upgrade && \ rm -rf /var/cache/apk/* -# Un-privileged user running the application -ARG DOCKER_USER -USER ${DOCKER_USER} - COPY --from=calendars-builder \ /home/frontend/apps/calendars/out \ - /usr/share/nginx/html + /srv -COPY ./apps/calendars/conf/default.conf /etc/nginx/conf.d -COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint +COPY ./Caddyfile /etc/caddy/Caddyfile -ENTRYPOINT [ "/usr/local/bin/entrypoint" ] - -CMD ["nginx", "-g", "daemon off;"] +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/src/frontend/Dockerfile.caddy b/src/frontend/Dockerfile.caddy new file mode 100644 index 0000000..0b384b5 --- /dev/null +++ b/src/frontend/Dockerfile.caddy @@ -0,0 +1,13 @@ +FROM caddy:2-alpine + +RUN apk update && \ + apk upgrade && \ + rm -rf /var/cache/apk/* + +COPY --chown=65534:65534 out /srv + +COPY --chown=65534:65534 ./Caddyfile /etc/caddy/Caddyfile + +USER nobody + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/src/frontend/Dockerfile.nginx b/src/frontend/Dockerfile.nginx deleted file mode 100644 index 88830fb..0000000 --- a/src/frontend/Dockerfile.nginx +++ /dev/null @@ -1,20 +0,0 @@ -FROM nginxinc/nginx-unprivileged:alpine3.22 - -# Upgrade system packages to install security updates -USER root -RUN apk update && \ - apk upgrade && \ - rm -rf /var/cache/apk/* - -# Un-privileged user running the application -ARG DOCKER_USER -USER ${DOCKER_USER} - -COPY out /usr/share/nginx/html - -COPY ./apps/calendars/conf/default.conf /etc/nginx/conf.d -COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint - -ENTRYPOINT [ "/usr/local/bin/entrypoint" ] - -CMD ["nginx", "-g", "daemon off;"] diff --git a/src/frontend/apps/calendars/.env b/src/frontend/apps/calendars/.env index 7ef3cda..907c9f4 100644 --- a/src/frontend/apps/calendars/.env +++ b/src/frontend/apps/calendars/.env @@ -1,2 +1,2 @@ NEXT_PUBLIC_API_ORIGIN= -NEXT_PUBLIC_VISIO_BASE_URL=https://visio.suite.anct.gouv.fr \ No newline at end of file +NEXT_PUBLIC_VISIO_BASE_URL= diff --git a/src/frontend/apps/calendars/.env.development b/src/frontend/apps/calendars/.env.development index d53e487..5809cd1 100644 --- a/src/frontend/apps/calendars/.env.development +++ b/src/frontend/apps/calendars/.env.development @@ -1,2 +1,2 @@ -NEXT_PUBLIC_API_ORIGIN=http://localhost:8921 +NEXT_PUBLIC_API_ORIGIN=http://localhost:8931 NEXT_PUBLIC_VISIO_BASE_URL=https://visio.suite.anct.gouv.fr diff --git a/src/frontend/apps/calendars/conf/default.conf b/src/frontend/apps/calendars/conf/default.conf deleted file mode 100644 index ed0be68..0000000 --- a/src/frontend/apps/calendars/conf/default.conf +++ /dev/null @@ -1,26 +0,0 @@ -server { - listen 8080; - listen 3000; - server_name localhost; - - root /usr/share/nginx/html; - - add_header X-Frame-Options DENY always; - - location / { - try_files $uri index.html $uri/ =404; - } - - location ~ "^/401/?$" { - try_files $uri /401.html; - } - - location ~ "^/403/?$" { - try_files $uri /403.html; - } - - error_page 404 /404.html; - location = /404.html { - internal; - } -} diff --git a/src/frontend/apps/calendars/eslint.config.mjs b/src/frontend/apps/calendars/eslint.config.mjs index 93ffbd0..6459c3f 100644 --- a/src/frontend/apps/calendars/eslint.config.mjs +++ b/src/frontend/apps/calendars/eslint.config.mjs @@ -1,19 +1,18 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); +import nextConfig from "eslint-config-next"; +import nextCoreWebVitals from "eslint-config-next/core-web-vitals"; +import nextTypescript from "eslint-config-next/typescript"; const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + ...nextConfig, + ...nextCoreWebVitals, + ...nextTypescript, { rules: { "react-hooks/exhaustive-deps": "off", + // TODO: fix these patterns to be React Compiler compatible + "react-hooks/set-state-in-effect": "warn", + "react-hooks/refs": "warn", + "react-hooks/preserve-manual-memoization": "warn", "@next/next/no-img-element": "off", }, }, diff --git a/src/frontend/apps/calendars/jest.config.ts b/src/frontend/apps/calendars/jest.config.ts index 8f83dcd..ef6be99 100644 --- a/src/frontend/apps/calendars/jest.config.ts +++ b/src/frontend/apps/calendars/jest.config.ts @@ -1,6 +1,4 @@ import type { Config } from "jest"; -import { pathsToModuleNameMapper } from "ts-jest"; -import tsconfig from "./tsconfig.json"; const config: Config = { preset: "ts-jest", @@ -11,10 +9,8 @@ const config: Config = { // Handle static assets FIRST (before path aliases) "\\.(css|less|scss|sass|svg|png|jpg|jpeg|gif)$": "/__mocks__/fileMock.js", - // Then handle path aliases - ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { - prefix: "/", - }), + // Path aliases (mirrors tsconfig.json paths) + "^@/(.*)$": "/src/$1", }, transform: { "^.+\\.(ts|tsx)$": [ diff --git a/src/frontend/apps/calendars/next.config.ts b/src/frontend/apps/calendars/next.config.ts index 32bc264..fa6f5b3 100644 --- a/src/frontend/apps/calendars/next.config.ts +++ b/src/frontend/apps/calendars/next.config.ts @@ -2,16 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "export", - debug: process.env.NODE_ENV === "development", - reactStrictMode: false, - webpack: (config, { isServer }) => { - // Resolve workspace packages - config.resolve.alias = { - ...config.resolve.alias, - - }; - return config; - }, + reactStrictMode: true, }; export default nextConfig; diff --git a/src/frontend/apps/calendars/package.json b/src/frontend/apps/calendars/package.json index c50d21d..3199659 100644 --- a/src/frontend/apps/calendars/package.json +++ b/src/frontend/apps/calendars/package.json @@ -4,55 +4,54 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --no-lint", + "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "build-theme": "cunningham -g css,scss,ts -o src/styles && mv src/styles/cunningham-tokens.scss src/styles/cunningham-tokens-sass.scss", "test": "jest", "test:watch": "jest --watch" }, "engines": { - "node": ">=22.0.0 <25.0.0", - "yarn": "1.22.22" + "node": ">=24.0.0 <25.0.0", + "npm": ">=10.0.0" }, "dependencies": { - "@event-calendar/core": "^5.2.3", + "@event-calendar/core": "^5.4.1", "@gouvfr-lasuite/cunningham-react": "4.2.0", - "@gouvfr-lasuite/ui-kit": "0.19.6", - "@tanstack/react-query": "5.90.10", + "@gouvfr-lasuite/ui-kit": "0.19.10", + "@tanstack/react-query": "5.90.21", "@tanstack/react-table": "8.21.3", "@viselect/react": "3.9.0", "clsx": "2.1.1", "date-fns": "4.1.0", - "i18next": "25.6.2", - "i18next-browser-languagedetector": "8.2.0", + "i18next": "25.8.14", + "i18next-browser-languagedetector": "8.2.1", "ical.js": "^2.2.1", - "next": "15.4.9", - "next-i18next": "15.4.2", + "next": "16.1.6", + "next-i18next": "15.4.3", "pretty-bytes": "7.1.0", - "react": "19.2.0", - "react-dom": "19.2.0", - "react-dropzone": "14.3.8", - "react-hook-form": "7.66.0", - "react-i18next": "16.3.3", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-dropzone": "15.0.0", + "react-hook-form": "7.71.2", + "react-i18next": "16.5.6", "react-toastify": "11.0.5", - "sass": "1.94.0", - "ts-ics": "^2.4.0", - "tsdav": "2.1.6" + "sass": "1.97.3", + "ts-ics": "^2.4.2", + "tsdav": "2.1.8" }, "devDependencies": { - "@eslint/eslintrc": "3.2.0", - "@tanstack/eslint-plugin-query": "5.66.1", - "@tanstack/react-query-devtools": "5.66.9", - "@types/jest": "29.5.14", - "@types/minimatch": "3.0.5", - "@types/node": "24.10.1", - "@types/react": "19.2.5", + "@gouvfr-lasuite/cunningham-tokens": "3.1.0", +"@tanstack/eslint-plugin-query": "5.91.4", + "@tanstack/react-query-devtools": "5.91.3", + "@types/jest": "30.0.0", +"@types/node": "24.12.0", + "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "eslint": "9.20.1", - "eslint-config-next": "15.1.7", - "jest": "29.7.0", - "ts-jest": "29.2.5", - "typescript": "5.4.5" + "eslint": "9.30.1", + "eslint-config-next": "16.1.6", + "jest": "30.2.0", + "ts-jest": "29.4.6", + "typescript": "5.9.3" } } diff --git a/src/frontend/apps/calendars/src/assets/home/banner.png b/src/frontend/apps/calendars/src/assets/home/banner.png deleted file mode 100644 index 840e972e28acc8f035458f05a47ac8d7b0d38385..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11214 zcmV;2lGP)+Ajf{qpki?(XjN^z`}p`S$kq|NsB@_xJnz z`}Otp@bK{d{{H&<`uOgwYV9_i`n=jZ3;<>lPm+~(%y z7Z;m4I;u-cu=ecj>~DAy5D(`vKIcMB@uH~nt+5FSl@Jh^_V)St?(gE_;^ZDH@Q;@4 zdV=KSU1A2?&#ulHJ|i^rfopcYp4DgyISq;Nali-rnEe-}30_^YiuQDmFSg zs3|BX*4EY>8X57LppcNw^ux#6+S(r-95OI3A|M}ZYilGVpg%vV`se8Mud@#g4C59f z=|fKA4jei;I6yx?{PFVj>+Fk)iQ(blt*orq*VpRm>g((BDk`Dw?)B$DOz1jB=yZJF z-`}aIsNn(;R4jwOIGGA zIqG+R>1=h8kdL{zxYN_q!ok7pX>te%l;%4|^1;OBX>vwJtmGsy=;-Ln%gfKs&Ltxv z>0oK}$jjs!C+>24;szD!WNiZjlF!f2%*@PtdU>FqpYZVT=srp4Mp0;JyI@{k;}apZ zwYBi8u;n#D{q^?i>+5A>V&>-NR8vxhg@r6DD$~-^^|ZL~hK}~q)$NUw?|z0bFr(k! z>`F*T}^5Np{v$*i*=kM?C_4M>{aBo~(wkIc`yu97*oT8SN z%Hu^)v$NIe;Niu^##mQXbacS)@AIpx*eEEWMnl2 z%4*&p>2L=QO1q1*9kl+9h2nPjW zk+yjN008!MQchE{va+(Wva+(Wva+(Wva+(Wva+(Wva0S(z3_?^2pZ2?D+b$%F@BJ-Qw-i*3H7Qz}Wlezt-aLx4yme+r9MG{n`Gqvee48 z=j7Pk_rk>Kw71*0;nDEC!_d6U((1S6xaqU!v&_r7xXAYIy0+irxZ}gh#=5-e&CuVn zv%IwDk1l>EgMw@S+&U z2LJ#kcu7P-RCwCtoKLLWMjglfXZ>$xp2vUIj(77&gGxeGmpl%Ss_!WftqOaoh|)@_ z-T_1vk#i_jr5pd*V)N43^Dxeh{=mDuhm8gU`fyC)WY7UIYf5&T& z$6jamp?;%8>$S7%^~b-z^V^lIRB9rrCWsRm-2zl*Dr9yF=}0G{{}9O&j&>^2YVv$V z2{f)ekKL@k9Pe#qU6yr}C_P6cPh3w4CdSHDs!MH_ZS|FE0Fzu?2e!XucBCye&`bFl zg2GM;MmI$TOgY1sii5Fw(NiAuA)%*uUIEryIiR*d<+dX$m7G-dT=mJs)xvIisx_Jj zJwUEL=Db^es015!LPK0BJd=EGF0b@&wv`)SuGY)y)-5pQr1?r1l#H5qY2xt6-IH2d zW^kDkp-zREo`~&1$sUznVtXR=?53SH%TPRJ)}o%&)q56rUxXC-RcGt>{{s2NLS)REIp98&OqVQ zQ^$n2E?B^9u}sjjsTC+gTNdL zFZH5G7o|6w_NT|#n=D$MB1Gtg9zLn`n8>GMXD~fyD7>7WEXu)~?S^Z?OA-~)b4=)I z#!02exK7m=MvnxW{$A58ss^1ta796AvspBe`@+jSrY`O_leC5WH)4Aym!36(?YW+Y z@_Mj`>6v+tLuPB{PTLcsSB-QDy1b>*=SCw*r?PR|49J4iwOa6aac^ z6|zYCx(B^lL(Uu|LN8D@P7kW0^lWwAYOg}i7?Qf=aZeV4N3#|Idgbjf6dp)Dr@wi6 zlDO?zBhb^dl-`h{LC$SYFn5u)`MjH+&e9veE^#ACtIQ8Ie1hqzaM%JvsU6=>=~bI! zq7_wson`)tL&?S8n}=QWbe5h8`op6^03lh4Jowq5S2V8&1sSGtC)rD-Q(4JEkhe!| zuhzw67A|xd4LN~&)K8v5%G5VLQ}moc2?CR3(Ar(DcCqsfLN6xtWVXXCub-qKV64&u zx)PmedQ86sk3eq_JX2=dAsLavx{8Gy;yL%Gld{5Sf;Y(WR^ZVPAHI=M;CPw zq5+VSr90##YuTGkvgsz;p^@csx&bju!JY3~S=(zIyduGP)AURea;Kl9c6iAF66{yA zW5e@(1XrIPWQ1t|%%_9sx;<@_&^2B_!XeU#=R|ZZ6{HJIiLnjKle#{rLta;zZQAx= zLaB8GLu$)eQhI$e7eX&k8t4Jj9G<-t4wXqll6^>v<5@;4t1|R~Z{^@ldL~e&fpa40 z!E_m4;+bCm$cGIridN_iOI`Ajv!Y2{4P@yivAu)cl%6cF)dd5cAC1@^3d7CPGu!B8 zGZ+#NUjKGWLNBb-b4;{0yu$KO)hdy8bSE$pi%{7aTF-b*FsrFe;x->WX7qSt+zTV$*<#iL zGZoWM636nA!;`eLSMc*!=v5L=r%zC#%WzpxOIMBxJOHJ%oSmbD9?0nRusl#XE0Elt z-(ldLdb}a)Byn$>^q{mdQ!nv`akIo!>f<^6Fw$FaW85zQR_F5SV|rO;?;RY>)8(b6 zDL#%2Ts>)`FLcr~HlMYywzqnO)H0DYFm>V68<-Aq7--iH=4imnuAZD2z1jBYH4TeO z9hTR#XyDRY8{SIXH2gvIni5PQdchWL&uXJ*^S)QnGw1Z9X$w8aG{?8S!^6G))wwa4 z-b^vJL$7&ETS`{cAxzVmCb#L78ftsLc&4hm2#rV2KRg|a&|@-8V=z5`OY~sNj6J27 z=Q0gxCoToW=uKyJdiSgP^i03Zv%~gIF9ql!Cc`ua(=(NA*`C!(&*Byh<(EIj=qd9R zdS7PnJ%Y!}UJQRJLeDWOHz6KQZ(BKM>sOO_lkfuqAfdP0Ne`Htq=%U5pjCo#Z!7d# zzgH&lEw2L48H?VC*^3Z8N2ufxpa-@U_XOzizl|-pQY1nTZI+&3wk$x;0h^9{w9hF* zPlD12b%O{!bEEVO!Da*7DSPh!iyq%?3eQ!aIC{dBYG;>vJM@G+=LiW)Nl%te3Oyjc zJaXepPnY`Dx%Y}4AG}cNmA76zd2!E{Amk=2jv-jEV;sgYq37`8-hkC2L3)UlX{@2V zNAdcDs;+F79-yUq2R+rcIo&c?Q8&G#xWo2}-pv@*I-yrth;SGIXB~R^Cw_wQsPArp zUi%UJ-)rezv11&?UG(y*c^rC+qkc6A)3YTJ;qDuD8po#1 zr8m}!1Cit|7IA(XtYv9+m7lprZ-X;s7NMtde@8?t=M;q}K{t*Mrhv84MtUQ#b0CTr zi)E0$n1de*ygI!dbz|_zZHWwG{o~Nbwaht95UYaV%-UvIA`e6y^Cbm22iNNbt%jlgdC^1HCob z{p;47&z-vR#DD+(8u!^;V zYno+&aEJU8FXv`453@>dbwu#DU%&g$GnXFuggpG}BWEhQbhm-toAgRPpk0wy-hL-s zEFx@=<~i9f8((MXnT{KTtH0V}zyJQ2`HF)OxrYpsybr0@TKP$$DGR^dB1w~YKZKU# z&*ys)vMl%7GBPsvxk9h*Hh=k#Z~v68{eAkagx;r&o@pq;UuCZ7Qy<)jZ!SGaP0!^A zk1~3iInj6Sov-NLuZtbuD0W=FeEIy(zt?Pg58nOw!;2R`{P^RG-z;hU;g5H|cI(QO zTQ5EP`LA3ZCE%GlNOE#gvN{AD20-G_>-L-c-yom?Zw?5v+1_+I-Q80R%T|{A8dRI; z)!=<{_pyB6W0cOyf1e^PCbK6`HMfM=3mN7yCPkic+=`Kj1+)HZtddTx^hJmv7fwJbf_ z+nrL)=e}gY-Nm7sT+kZ5+80uO^Ph5`2t5q7#r5MAz4+thVi(vX{GDfaTwqT2TpPVR ztU2fO)T^hC8};BvMfiJ--E;lzwVWo);&2CE9*v%D0p;JV^z@x5(A6e- z$H%V?g9i}_co0lnJ)6hDS<4mL1T5h;o8rJX!=Eh{4i670OoFq#BXW_)x1MMgv-FG-`4;p%0#O=KIW? zxpTMoq}?>^vb}eH_y7Gr|G(*V|1lJLDAW>0I)ZYJNv{(a9OngwRYtG(-ry0Qh?_V> z$B8a)WD*d__K|tzz*aSLFnSte*f9jgOgF%v&}P7AP7t($R?H8*f90#Cc}{e0tk$6S zMO+q@Wg8p3!ziGX9Y-k8+j*ZOw`pbMEPXNny&cILdpS?_k#5ExGum<YW_&(npiOpZT zrI%-^CCh%H7CqcS7#h3|R3rh9xeJH_gQqsTGzd{9r)2}rvBm0{5P z=Lk~Kwqpov*$eGFZ*YegKXJac!6Q)uxei>s4{BLj_7WJWrN{MJfE(d!!?RL@H?|II zF`r@=kP}+J7+?hDi`Fu3N75-vb<#=A{T(HhN+vqIBD zr&U4EmOoytM-S*TD<^$LQH|V~%^235x!G&EAY>@?&S{vP;8f6y-+I8%oW0(+YZ*MG zEG-EuO{_o2Mv_daQY#raU}ApcY6FlUgab*Q2K9D#o2-~dtHqa}2q0fBdscLz=z}7? z)iR&6y|jd)1X~cqc%#$uXl?@1vOIix(}o+)sAFr+h8LO$?6thR@8F4_(myf#dJTie zaiPHzGH)p@RRYM^`(P3y2y9#ctZ(x)Xt%oK$dS~2TJG`^m=V7`y&pl7z`OKjf!#N5 zJ&Qhh&)&=Z;l&jcC0LxpscL*@_Wfg9rrG5qirxinO_~!aO+xSCD-3$a8x7C%eB`1% znf-ad8=`P$j{cc`ITuY8MO(A+m^?AL6r4+BvG;ah^9K~(s z>jeQ{d3>M5etCNTnRFC@K4(Rryi0```<3>V=ZMrIi^G*i?>eb@k1^MEQkHl-*No zofC5xWRFIgY2mf4cly%%bj*Qp?ev6vMtp;wPJMOK-F`?2dC%oIDiA$WS;ND#LM0sG z#iJF{LPdRa?jNMexKlAzHT*62yzi zkUBE%&gOsrhZQwE>@~}rBk1&nY@5XAux}CtD$zJH$uVS5!OyaB9cXOaMFtOvT!U;( zBZ?oLMm4~Il08WgCm$USsCeOnBssLHPkk>>4@Z0Rx%*xey&cf)cAIuodM~Gko&z=U zV^=b#kfw+Cf$8XLB!Z4(JlLC@n>+Jmq_^1VXdRCe!i(b6_=afOm{-dWNqcZjAiIVN zO{UF*mOHV|N3Ngid-QTpuaEpu^g5np`iK8ZIW1R{Pv5}wdb8WJ0|n@4-BPSpx9f$C zQf2fYQYV}LP0>qcM3&Ga|Ke?Rp#UE5G0(5dm8NWPVc@4ZYCy_jp!Otn2Z zkt;` znTph!Y3oXXUi@UM=n(-F+4OoFN|dHR1dkJ9M=ay4Nd4jEcBAH;7^w5S2q=EYVe{8b zxB%)#!(+koiGNa;UaQ&ZP_lO~c9u<(e`gK!dKW6`NfOlDiEg0L^c1ZzK8`{lo3?vN zgi=Y5oCE zI0-Y-END&w51L%}Q58LMy`+TRXNxZ}=bpQSf7Ix9LksqY}0W6CK^OI0rdbUM0LF#o3o$v)K%8 z&|V9E{fdO5iT|;^mZ;I=`LYMKoOeROxmvesdZw%nVL1+>)$U(mSALAGazP zBHq&~q*>aGr&-6~QJpPCxKM0$+|c|Xn-I)w<4mCA1-nkA@S!*YAHN2@poJi}sEFZP zlBHo81(ifQIu^1jgq}j1ps7R5gnYKYTGIv1Y@fw*j~$qhLAK~ zTUH*t~q&RrzI0)!RIx199_2V4IQ< zZ??v(=Uu!XyWtLG`wH~P`4UC%2OqDT9I=5d6+0v7^UJw2CHSsI-@Qit`H`rt2YeSj zBbuJ*MEgr?(mN9+%I#dM&-Cc%=rlg)t9YNAez=RAhj)>%iH0ki$l$r0iW`p70&Ef` zREix0A=%PlR}8OI)0^8mdqzTom)+Wnx0HuF>&x*Gf}wqM{h*2--I28V^lz7N&%WKC zo+RCV{>Ggh9d@)Bc21|`dX8*Ak=0$EXoWi%ZKbb;QO>}1x~GnHs6hyjb9 zf1^+#diu<*%aW?sp+_iD1-0wZpIRNwaEl~BO< zr**@}yT29>0hVv$@ZmAefpIB69fjv=Tz{o%3H6jFYepdZuDS)VdnD2eD2XgO8Hxrg z2E>AhvoPhd$6qX|&c2G`fy*`NNu&Aax{)!wBk4OUp|>@G#H9Bw{d-jnlHlaGhb<4$=Pb z(m1}HUhrH=XT+<;JSUqT<*7fPKFwj$`#!^;T%UU#iw92ce=QZ$-S=@W8y6$+^TXbX zm<-7nEJz@&5X>oaka=myvZLYMWEC@$USD`D*$bXoE4{a`Y}|O|$btOReC2lh?fJp! z>1=xYYSE+5)O>8<@B>t3y;#Gmn~tuyJPS@=5Z|B*nq005L(V%+H749j6tS%NrkEm*$Y+)|as|&A4 z&yU>F7uHCxG+Mu&%#;B?8yY?Iu+OiJUaz-BrLG90hZ#n!wp70f6IR`l2dvwBnuHxE z99fbj=#l?a5WULyfkBUtV~UT4NN*~cDT7}8>CaCuouj*Zjr4lQr{gU!dM-GxtusO8 zxR0t{YD_O3N5$M<@5B&s99WZHnlAnG@aVmteetDNCI_N7n*QImRL~>&8)lyK?P!z? zp5c@P=WKSc*qqsCJOyBl93ZkMa@6&Z)nZ%yKX>Q$(?)g%@MApK%*B_nmxxp=Rdv*dR@zD!Y4-tDMQ&F%R#mpLTgya! zLJJrJAyEY)-50<~wRs@vE=tqIz-}6)gs|*xD*|Mrws{EMs>++grs~T|y{P*q%t%M- z_njHrVy~|3RTDml_j)i( zww8@hkZdNo>kaJu4b$uX+wbbo^Hz)~E~EFxN7e5B$wlk*z`&bkk@{Do^d9~?@3?D= zV>U8Y%#y!SZ@m_Qb8)f*TX}>!q9+%ct_!8uZ3LTA+jh4c?r%Q-nHjzBR1W4Zq4)F4 z->h-}uV1Q5@27>FROUIrssOrH`0#T1WAc{^#muiBEsYumGWJHarrXMTT6kD~9H+g4 z9>@B!Flex84Q3`5(C4abJafrN@9sZ-QY_E^T`8QufA@L5ofGMmHeWxlytQJq{>G(8 zb0&50hp(M8*gvHA-LjgGbRi-O$EaV5<>8bq7TTnLNBmPac4agb75`VDCv~1)^Q%C58Z>2=={2@K zk3shA_JaGV(UWZ^+3b2WdPXH#%b%&-;|%`;zqFI|(49-paNQ1}i8bT%jXw2Gl1wf& zb#V+v+9E(`q_ zCS^3N(#Vsxv#X?6RHPt#rU}d)widzTP<3|7Yhh+(hSCY7*DiG#4kpLP+ZNz&|ZR*rK)DHRuTu+6=d-wE9y++7uJ}&Vu4<-!%9Y1 zCVToCDrLa}+cUbQ6>qfnVEN9x+_3D451x?4fl0PMz4U}_=LoqE7U%lFAR+w&$-E}p z8(7v3dynV)bEsH28Kd;%{7t7b3z7N#Yz`28J3DPBqSMnqKXc^tIP)1fyyNo=x!eP6 z)R`O8e^kun!1&EAO%syUOx$$#ymq$fRoZ|40U;UL3*3tY)`sak ze>9a|0NG1s@avJljMF&~h(Xvr8`#F$$6p;;Ef~tUe{0aY>FPt!)ektZcyh*BGj#$pQqCF!#T zG+??b%LZP+AvUmlT`!f=V>!gl1EqdSqlBzethFsA0rW!jI?vD2z)gN-gvNP(jSlnt zJxY&xeo>(yhZ!|;{N6M_Na;;k+Eqi#PUmKj4v1nx*@6>I&=?_OY6yOOLW$_7Ii~PF zONT)y1L{tWWK%RWhbMoZkVzT@wK=7Npbo3i{DlQ}aC=={uhUNL*2^gtAg@CYM|ZP) z*G=TSuqe5L1ct9j&s~UH+-S;iBi5oP;x?Go=!suy^wb^rg)It7bdrWXLb=O1J!C|#gObb1FgFccmR(UH8-q=6N(i8?SzgGZ<|DP1g~w@!zE zF}`PIkTQ_t>KY`_rpxURFVPnbM5}P3eiP$cIy#UXScYwJB&b zCIqQ{@T4L=^M<104G;3f4)pfzc6|Kp_8~O^k8%J7&Oq-v(o52j!qU=6pA z_|c8kXQlLnH5#1H_8(HD2fsD3v@nG_Z-vqmLT+wDxV<+LS|(zE&W`S9F(Sz*;pEcz z6zW$JQg#PNV;X6YPNqUnf-#5`cA3h|(KR!At;r;29`98qwQC1HTr#Mu5qc|Bp>7@& z);`aVQM$zQ%RyBIe=^Ek%nWSPbd%@7NBofI7vSgsRS`9KDENKwIg_$?DXggAnlgH1 z1NUkANMjKUsA>wmPn4)Q#iK%S)Zz#nq^N0>t$7w0s4V*1hkwT56)s{CoR~2FZ(N

lxe#sG*51Mvp{8G`Ood9vB~l*1@1nW@1MqmkRF0 z;t=RT^!7)b)6%81?`wjPh8PE8DNJR_pA&8%O1v?{%prtdSMC%dGIX84}w zywAGyY`u-pt6H*ywLg0>c%w|Q*kqhiu(VG!uLa8e{cNF%j|Pj?9aT^8C8M(V{Q)zD zx^p>L8M=YyRaPs;NEgQqwKn&nndbCK-Wth}1za#e8!BJS-;7(y*o|8yt+OO}rKROV@=Gz;c#$XnVa*9JA&>(<^kEo`-cBOwojSR~-Zlsf68hKco3!g{#(p*S2;r9qy zKBoUEdhY)hdbRW^3-2xH-5$Z!*HPJXo|c~2$nc@g+r0+pHNmy)Fr?RsOv)u*OH=g3 zW;zC52tB!_3Fzz4JLBr4>2Y3(6(8Eza+H^J?*~)$#`FWjO zN9pkp<20c-n-!oJM$c22Ypsv5(#Khuw!zdKQkSnWJ8Pc6E>`lN_Reg(aS#Te_$Xs+ zt0UagGyngqgHT*AiE$ExXqfRsAJT=Ii-cAH$O(`VK}WZG*83y#&i_R7)uOJ|!u0xW+@C!;CW=Yx zPxR@5_4M3JX0;bhlwSJ>yPrJ0_O~v*u?zHe-+sR1r+-V?b00C;JF3cm4!8A#Y0~SD z=!qOc8h9nF7S~=-s~^%8p@)YfCQ7Fk$nUwPIh{j!Tvj`6dIO$t0e(KcvFr3Ykzr7# zH;e|T%j8*+5~ zZ;d)}tUSF@nO>#|t(M--Vh=^CR-GR871Q%(Sz>y7FThSQ(}Vx?($TY{y(#5}ImY>{ zMH6#;!PD!irk823ea(s21jB0GkddP9MKffA#3pg-SC<}IO?tUrZD00`&|AyQCi2G+ zaiR(9_$HT#f9E3({(a!Yz3RS)n*K>X$0tH>^kR0jYG5N(SCO3hT$w$=;XyLQ*B+7P zYa67B=>bRn9}tHdF!V|R2=T={s3v@rta1ED(jzWA3GpPA&B!ZhKaR%-8Z*i-m(vg| zOz*1wfK4--U!hTuI@YnMF`<6TqoCNsmR%h^lDYs4X$`VNUKK-fZ3GK@h`VH-Pzp9Q ze}(U{YofQT>0nVXHlsC2qQB@m8iB!_nHw1{=oVR%@ZQ?f+-gng7x*A-OS#nNRg0|- zI?TwAeVS>s_ZGUyz6V=Bo6zN0qvAY@z%7nHrA(=4TCh`qM@8ttz2js79VDMj*>q@g z5d;rs(k~VnE?tf057nHv0n9MVCb2!ZGT3=1!aPTs%w2|FB{Zi5EZ65`!ekjHGO)Q`dG}i!GEGX4{gALDfC1{OdhYH3 zn-f4o93+6F6l#rh=a3{;YoM?1iP3{ad-l|;eLlXV?PAfSmj4xc{Z3cCWDHJ4lc9tR sP?dt$$1+5Rrv7X5SU?DZUjP6A diff --git a/src/frontend/apps/calendars/src/assets/home/banner.svg b/src/frontend/apps/calendars/src/assets/home/banner.svg new file mode 100644 index 0000000..502c226 --- /dev/null +++ b/src/frontend/apps/calendars/src/assets/home/banner.svg @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/apps/calendars/src/features/api/fetchApi.ts b/src/frontend/apps/calendars/src/features/api/fetchApi.ts index e704f7a..b008479 100644 --- a/src/frontend/apps/calendars/src/features/api/fetchApi.ts +++ b/src/frontend/apps/calendars/src/features/api/fetchApi.ts @@ -30,47 +30,14 @@ export const fetchAPI = async ( }); } const csrfToken = getCSRFToken(); + const isFormData = init?.body instanceof FormData; const response = await fetch(apiUrl, { ...init, credentials: "include", headers: { ...init?.headers, - "Content-Type": "application/json", - ...(csrfToken && { "X-CSRFToken": csrfToken }), - }, - }); - - if (response.ok) { - return response; - } - - const data = await response.text(); - - if (isJson(data)) { - throw new APIError(response.status, JSON.parse(data)); - } - - throw new APIError(response.status); -}; - -export const fetchAPIFormData = async ( - input: string, - init?: RequestInit & { params?: Record }, -) => { - const apiUrl = new URL(`${baseApiUrl("1.0")}${input}`); - if (init?.params) { - Object.entries(init.params).forEach(([key, value]) => { - apiUrl.searchParams.set(key, String(value)); - }); - } - const csrfToken = getCSRFToken(); - - const response = await fetch(apiUrl, { - ...init, - credentials: "include", - headers: { - ...init?.headers, + ...(!isFormData && { "Content-Type": "application/json" }), ...(csrfToken && { "X-CSRFToken": csrfToken }), }, }); diff --git a/src/frontend/apps/calendars/src/features/auth/types.ts b/src/frontend/apps/calendars/src/features/auth/types.ts index 22e128b..61ad618 100644 --- a/src/frontend/apps/calendars/src/features/auth/types.ts +++ b/src/frontend/apps/calendars/src/features/auth/types.ts @@ -1,13 +1,23 @@ +export interface Organization { + id: string; + name: string; +} + /** * Represents user retrieved from the API. * @interface User * @property {string} id - The id of the user. * @property {string} email - The email of the user. - * @property {string} name - The name of the user. + * @property {string} language - The language of the user. + * @property {boolean} can_access - Whether the user can access the app. + * @property {boolean} can_admin - Whether the user can administer resources. + * @property {Organization} organization - The user's organization. */ export interface User { id: string; email: string; language: string; can_access: boolean; + can_admin: boolean; + organization?: Organization; } diff --git a/src/frontend/apps/calendars/src/features/calendar/api.ts b/src/frontend/apps/calendars/src/features/calendar/api.ts index 82fe19e..521d43c 100644 --- a/src/frontend/apps/calendars/src/features/calendar/api.ts +++ b/src/frontend/apps/calendars/src/features/calendar/api.ts @@ -2,66 +2,64 @@ * API functions for calendar operations. */ -import { fetchAPI, fetchAPIFormData } from "@/features/api/fetchApi"; +import { fetchAPI } from "@/features/api/fetchApi"; /** - * Subscription token for iCal export. + * Channel response from the API. */ -export interface SubscriptionToken { - token: string; - url: string; +export interface Channel { + id: string; + name: string; + type: string; + role: string; caldav_path: string; - calendar_name: string; + url: string | null; is_active: boolean; - last_accessed_at: string | null; + last_used_at: string | null; created_at: string; + settings: Record; } /** - * Parameters for subscription token operations. + * Channel with token (returned on creation). */ -export interface SubscriptionTokenParams { - caldavPath: string; - calendarName?: string; +export interface ChannelWithToken extends Channel { + token: string; } /** - * Error types for subscription token operations. + * Error types for channel operations. */ -export type SubscriptionTokenError = +export type ChannelError = | { type: "not_found" } | { type: "permission_denied"; message: string } | { type: "network_error"; message: string } | { type: "server_error"; message: string }; /** - * Result type for getSubscriptionToken - either a token, null (not found), or an error. + * Result type for getICalFeedChannel. */ -export type GetSubscriptionTokenResult = - | { success: true; token: SubscriptionToken | null } - | { success: false; error: SubscriptionTokenError }; +export type GetICalFeedResult = + | { success: true; channel: Channel | null } + | { success: false; error: ChannelError }; /** - * Get the subscription token for a calendar by CalDAV path. - * Returns a result object with either the token (or null if not found) or an error. + * Get the ical-feed channel for a calendar by CalDAV path. */ -export const getSubscriptionToken = async ( - caldavPath: string -): Promise => { +export const getICalFeedChannel = async ( + caldavPath: string, +): Promise => { try { const response = await fetchAPI( - `subscription-tokens/by-path/?caldav_path=${encodeURIComponent(caldavPath)}`, - { method: "GET" } + `channels/?type=ical-feed`, + { method: "GET" }, ); - return { success: true, token: await response.json() }; + const channels: Channel[] = await response.json(); + const match = channels.find((c) => c.caldav_path === caldavPath) ?? null; + return { success: true, channel: match }; } catch (error) { if (error && typeof error === "object" && "status" in error) { const status = error.status as number; - // 404 means no token exists yet - this is expected - if (status === 404) { - return { success: true, token: null }; - } - // Permission denied if (status === 403) { return { success: false, @@ -71,7 +69,6 @@ export const getSubscriptionToken = async ( }, }; } - // Server error if (status >= 500) { return { success: false, @@ -82,7 +79,6 @@ export const getSubscriptionToken = async ( }; } } - // Network or unknown error return { success: false, error: { @@ -94,14 +90,17 @@ export const getSubscriptionToken = async ( }; /** - * Create or get existing subscription token for a calendar. + * Create an ical-feed channel (get-or-create semantics on the backend). */ -export const createSubscriptionToken = async ( - params: SubscriptionTokenParams -): Promise => { - const response = await fetchAPI("subscription-tokens/", { +export const createICalFeedChannel = async (params: { + caldavPath: string; + calendarName?: string; +}): Promise => { + const response = await fetchAPI("channels/", { method: "POST", body: JSON.stringify({ + name: params.calendarName || params.caldavPath, + type: "ical-feed", caldav_path: params.caldavPath, calendar_name: params.calendarName || "", }), @@ -110,17 +109,12 @@ export const createSubscriptionToken = async ( }; /** - * Delete (revoke) the subscription token for a calendar. + * Delete a channel by ID. */ -export const deleteSubscriptionToken = async ( - caldavPath: string -): Promise => { - await fetchAPI( - `subscription-tokens/by-path/?caldav_path=${encodeURIComponent(caldavPath)}`, - { - method: "DELETE", - } - ); +export const deleteChannel = async (channelId: string): Promise => { + await fetchAPI(`channels/${channelId}/`, { + method: "DELETE", + }); }; /** @@ -135,22 +129,94 @@ export interface ImportEventsResult { } /** - * Import events from an ICS file into a calendar. + * Response from the import-events endpoint (queued task). */ -export const importEventsApi = async ( +interface ImportTaskResponse { + task_id: string; +} + +/** + * Task status response from the polling endpoint. + */ +export interface TaskStatus { + status: "PENDING" | "PROGRESS" | "SUCCESS" | "FAILURE"; + result: { + status: string; + result: ImportEventsResult | null; + error: string | null; + } | null; + error: string | null; + progress?: number; + message?: string; +} + +/** + * Start an ICS import task. Returns the task_id for polling. + */ +export const startImportTask = async ( caldavPath: string, file: File, -): Promise => { +): Promise => { const formData = new FormData(); formData.append("file", file); formData.append("caldav_path", caldavPath); - const response = await fetchAPIFormData( - "calendars/import-events/", - { - method: "POST", - body: formData, - }, - ); + const response = await fetchAPI("calendars/import-events/", { + method: "POST", + body: formData, + }); + const data: ImportTaskResponse = await response.json(); + return data.task_id; +}; + +/** + * Poll the status of a task. + */ +export const getTaskStatus = async (taskId: string): Promise => { + const response = await fetchAPI(`tasks/${taskId}/`, { method: "GET" }); return response.json(); }; + +/** + * Poll a task until it completes. Returns the import result. + */ +export const pollImportTask = async ( + taskId: string, + onProgress?: (progress: number, message?: string) => void, +): Promise => { + const POLL_INTERVAL = 1000; // 1 second + + return new Promise((resolve, reject) => { + const poll = async () => { + try { + const status = await getTaskStatus(taskId); + + if (status.status === "PROGRESS") { + onProgress?.(status.progress ?? 0, status.message); + setTimeout(poll, POLL_INTERVAL); + return; + } + + if (status.status === "PENDING") { + setTimeout(poll, POLL_INTERVAL); + return; + } + + if (status.status === "SUCCESS" && status.result?.result) { + resolve(status.result.result); + return; + } + + reject( + new Error( + status.result?.error ?? status.error ?? "Import failed", + ), + ); + } catch (error) { + reject(error); + } + }; + + poll(); + }); +}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/SubscriptionUrlModal.tsx b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/SubscriptionUrlModal.tsx index 7fdbb83..ea93e9e 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/SubscriptionUrlModal.tsx +++ b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/SubscriptionUrlModal.tsx @@ -8,9 +8,9 @@ import { useTranslation } from "react-i18next"; import { Button, Modal, ModalSize } from "@gouvfr-lasuite/cunningham-react"; import { - useCreateSubscriptionToken, - useDeleteSubscriptionToken, - useSubscriptionToken, + useCreateICalFeedChannel, + useDeleteICalFeedChannel, + useICalFeedChannel, } from "../../hooks/useCalendars"; interface SubscriptionUrlModalProps { @@ -31,21 +31,24 @@ export const SubscriptionUrlModal = ({ const [showRegenerateConfirm, setShowRegenerateConfirm] = useState(false); const [hasTriedCreate, setHasTriedCreate] = useState(false); - const { token, tokenError, isLoading } = useSubscriptionToken(caldavPath); - const createToken = useCreateSubscriptionToken(); - const deleteToken = useDeleteSubscriptionToken(); + const { channel, channelError, isLoading } = useICalFeedChannel(caldavPath); + const createChannel = useCreateICalFeedChannel(); + const deleteChannelMutation = useDeleteICalFeedChannel(); - // Use token from query or from mutation result (whichever is available) - const displayToken = token || createToken.data; - // Show error from token fetch or from creation failure - const hasRealError = tokenError || (createToken.error && hasTriedCreate); - const isRegenerating = deleteToken.isPending || createToken.isPending; - const showLoading = isLoading || createToken.isPending; + // Use channel from query or from mutation result (whichever is available) + const displayChannel = channel || createChannel.data; + const displayUrl = displayChannel?.url; + // Show error from channel fetch or from creation failure + const hasRealError = + channelError || (createChannel.error && hasTriedCreate); + const isRegenerating = + deleteChannelMutation.isPending || createChannel.isPending; + const showLoading = isLoading || createChannel.isPending; // Get appropriate error message based on error type const getErrorMessage = (): string => { - if (tokenError) { - switch (tokenError.type) { + if (channelError) { + switch (channelError.type) { case "permission_denied": return t("calendar.subscription.errorPermission"); case "network_error": @@ -66,33 +69,39 @@ export const SubscriptionUrlModal = ({ } }, [isOpen]); - // Create token on first open if none exists (only try once) - // We also try to create if there was an error (404 means no token exists) + // Create channel on first open if none exists (only try once) useEffect(() => { if ( isOpen && - !token && + !channel && !isLoading && - !createToken.isPending && + !createChannel.isPending && !hasTriedCreate ) { setHasTriedCreate(true); - createToken.mutate({ caldavPath, calendarName }); + createChannel.mutate({ caldavPath, calendarName }); } - }, [isOpen, token, isLoading, createToken, caldavPath, calendarName, hasTriedCreate]); + }, [ + isOpen, + channel, + isLoading, + createChannel, + caldavPath, + calendarName, + hasTriedCreate, + ]); const handleCopy = async () => { - const url = displayToken?.url; - if (!url) return; + if (!displayUrl) return; try { - await navigator.clipboard.writeText(url); + await navigator.clipboard.writeText(displayUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Fallback for older browsers const textArea = document.createElement("textarea"); - textArea.value = url; + textArea.value = displayUrl; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); @@ -104,8 +113,10 @@ export const SubscriptionUrlModal = ({ const handleRegenerate = async () => { setShowRegenerateConfirm(false); - await deleteToken.mutateAsync(caldavPath); - await createToken.mutateAsync({ caldavPath, calendarName }); + const channelId = displayChannel?.id; + if (!channelId) return; + await deleteChannelMutation.mutateAsync(channelId); + await createChannel.mutateAsync({ caldavPath, calendarName }); }; return ( @@ -130,17 +141,17 @@ export const SubscriptionUrlModal = ({

{t("calendar.subscription.loading")}
- ) : hasRealError && !displayToken ? ( + ) : hasRealError && !displayUrl ? (
{getErrorMessage()}
- ) : displayToken?.url ? ( + ) : displayUrl ? ( <>
(e.target as HTMLInputElement).select()} /> diff --git a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/utils.ts b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/utils.ts index bb8bf0f..2ff3c18 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/utils.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/utils.ts @@ -1,7 +1,7 @@ /** * Extract the CalDAV path from a full calendar URL. * - * URL format: http://localhost:8921/api/v1.0/caldav/calendars/user@example.com/uuid/ + * URL format: http://localhost:8931/api/v1.0/caldav/calendars/user@example.com/uuid/ * Returns: /calendars/user@example.com/uuid/ */ export const extractCaldavPath = (calendarUrl: string): string | null => { diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx index f88b32a..db37a5f 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx @@ -18,9 +18,11 @@ import { RecurrenceSection } from "./event-modal-sections/RecurrenceSection"; import { LocationSection } from "./event-modal-sections/LocationSection"; import { VideoConferenceSection } from "./event-modal-sections/VideoConferenceSection"; import { AttendeesSection } from "./event-modal-sections/AttendeesSection"; +import { ResourcesSection } from "./event-modal-sections/ResourcesSection"; import { DescriptionSection } from "./event-modal-sections/DescriptionSection"; import { InvitationResponseSection } from "./event-modal-sections/InvitationResponseSection"; import { SectionPills } from "./event-modal-sections/SectionPills"; +import { useResourcePrincipals } from "@/features/resources/api/useResourcePrincipals"; import type { EventModalProps, RecurringDeleteOption } from "./types"; import { SectionRow } from "./event-modal-sections/SectionRow"; @@ -41,13 +43,22 @@ export const EventModal = ({ const [isLoading, setIsLoading] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const { resources: availableResources } = useResourcePrincipals(); + const organizer: IcsOrganizer | undefined = event?.organizer || (user?.email ? { email: user.email, name: user.email.split("@")[0] } : undefined); - const form = useEventForm({ event, calendarUrl, adapter, organizer, mode }); + const form = useEventForm({ + event, + calendarUrl, + adapter, + organizer, + mode, + availableResources, + }); // Check if current user is invited const currentUserAttendee = event?.attendees?.find( @@ -57,7 +68,7 @@ export const EventModal = ({ const isInvited = !!( event?.organizer && currentUserAttendee && - event.organizer.email !== user?.email + event.organizer.email?.toLowerCase() !== user?.email?.toLowerCase() ); const currentParticipationStatus = currentUserAttendee?.partstat || "NEEDS-ACTION"; @@ -72,7 +83,11 @@ export const EventModal = ({ setIsLoading(true); try { const icsEvent = form.toIcsEvent(); - await onSave(icsEvent, form.selectedCalendarUrl); + + await onSave( + icsEvent, + form.selectedCalendarUrl, + ); onClose(); } catch (error) { console.error("Failed to save event:", error); @@ -152,8 +167,17 @@ export const EventModal = ({ icon: "group", label: t("calendar.event.attendees"), }, + ...(availableResources.length > 0 + ? [ + { + id: "resources" as const, + icon: "meeting_room", + label: t("calendar.event.sections.addResources"), + }, + ] + : []), ], - [t, visioBaseUrl], + [t, visioBaseUrl, availableResources.length], ); return ( @@ -276,11 +300,21 @@ export const EventModal = ({ )} + {availableResources.length > 0 && + form.isSectionExpanded("resources") && ( + + )} {form.isSectionExpanded("description") && (
{alarms.map((alarm, index) => ( -
+
({ + value: r.id, + label: `${r.name} (${t(`resources.types.${r.resourceType.toLowerCase()}`)})`, + }))} + value={undefined} + onChange={handleSelect} + clearable={false} + variant="classic" + fullWidth + placeholder={t("calendar.resources.placeholder")} + /> +
+ )} + + {selectedResources.length > 0 && ( +
+ {selectedResources.map((resource) => ( + + + {getResourceIcon(resource.resourceType)} + + {resource.name} + + + ))} +
+ )} +
+ + ); +}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useEventForm.ts b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useEventForm.ts index 4805705..aa554e3 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useEventForm.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useEventForm.ts @@ -10,6 +10,7 @@ import type { IcsOrganizer, } from "ts-ics"; import type { EventCalendarAdapter } from "../../../services/dav/EventCalendarAdapter"; +import type { ResourcePrincipal } from "@/features/resources/api/useResourcePrincipals"; import type { AttachmentMeta, EventFormSectionId } from "../types"; import { cleanEventForDisplay } from "../utils/eventDisplayRules"; import { @@ -27,6 +28,7 @@ interface UseEventFormParams { adapter: EventCalendarAdapter; organizer: IcsOrganizer | undefined; mode: "create" | "edit"; + availableResources?: ResourcePrincipal[]; } export const useEventForm = ({ @@ -35,6 +37,7 @@ export const useEventForm = ({ adapter, organizer, mode, + availableResources = [], }: UseEventFormParams) => { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); @@ -46,6 +49,7 @@ export const useEventForm = ({ const [selectedCalendarUrl, setSelectedCalendarUrl] = useState(calendarUrl); const [isAllDay, setIsAllDay] = useState(false); const [attendees, setAttendees] = useState([]); + const [resources, setResources] = useState([]); const [recurrence, setRecurrence] = useState( undefined, ); @@ -80,13 +84,6 @@ export const useEventForm = ({ setAlarms(event?.alarms || []); setAttachments([]); - // Initialize attendees - if (event?.attendees && event.attendees.length > 0) { - setAttendees(event.attendees); - } else { - setAttendees([]); - } - // Initialize recurrence if (event?.recurrenceRule) { setRecurrence(event.recurrenceRule); @@ -175,6 +172,42 @@ export const useEventForm = ({ } }, [event, calendarUrl, mode, organizer?.email]); + // Separate effect for resource/attendee splitting — only depends on + // event.attendees and availableResources, avoids resetting the entire + // form when the resource list loads asynchronously. + useEffect(() => { + if (event?.attendees && event.attendees.length > 0) { + const resourceEmails = new Set( + availableResources + .filter((r) => r.email) + .map((r) => r.email!.toLowerCase()), + ); + const peopleAttendees: IcsAttendee[] = []; + const matchedResources: ResourcePrincipal[] = []; + + for (const att of event.attendees) { + const email = att.email.toLowerCase(); + const resource = availableResources.find( + (r) => r.email && r.email.toLowerCase() === email, + ); + if (resource && resourceEmails.has(email)) { + matchedResources.push(resource); + } else { + peopleAttendees.push(att); + } + } + + setAttendees(peopleAttendees); + setResources(matchedResources); + if (matchedResources.length > 0) { + setExpandedSections((prev) => new Set([...prev, "resources"])); + } + } else { + setAttendees([]); + setResources([]); + } + }, [event?.attendees, availableResources]); + const toggleSection = useCallback((sectionId: EventFormSectionId) => { setExpandedSections((prev) => { const next = new Set(prev); @@ -249,6 +282,25 @@ export const useEventForm = ({ // eslint-disable-next-line @typescript-eslint/no-unused-vars const { duration: _duration, ...eventWithoutDuration } = event ?? {}; + // Merge resource attendees back into the attendees list + const resourceAttendees: IcsAttendee[] = resources + .filter((r) => r.email) + .map((r) => { + // Preserve partstat from the original event if available + const existing = event?.attendees?.find( + (a) => a.email.toLowerCase() === r.email!.toLowerCase(), + ); + return { + email: r.email!, + name: r.name, + partstat: existing?.partstat ?? "NEEDS-ACTION", + rsvp: false, + role: "NON-PARTICIPANT" as const, + cutype: r.resourceType, + }; + }); + const allAttendees = [...attendees, ...resourceAttendees]; + if (isAllDay) { const startDate = parseDateLocal(startDateTime); @@ -278,7 +330,7 @@ export const useEventForm = ({ start: { date: utcStart, type: "DATE" }, end: { date: utcEnd, type: "DATE" }, organizer, - attendees: attendees.length > 0 ? attendees : undefined, + attendees: allAttendees.length > 0 ? allAttendees : undefined, recurrenceRule: recurrence, alarms: alarms.length > 0 ? alarms : undefined, url: videoConferenceUrl || undefined, @@ -337,7 +389,7 @@ export const useEventForm = ({ }, }, organizer, - attendees: attendees.length > 0 ? attendees : undefined, + attendees: allAttendees.length > 0 ? allAttendees : undefined, recurrenceRule: recurrence, alarms: alarms.length > 0 ? alarms : undefined, url: videoConferenceUrl || undefined, @@ -355,6 +407,7 @@ export const useEventForm = ({ location, organizer, attendees, + resources, recurrence, alarms, videoConferenceUrl, @@ -382,6 +435,8 @@ export const useEventForm = ({ // Complex fields attendees, setAttendees, + resources, + setResources, recurrence, setRecurrence, alarms, diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerHandlers.ts b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerHandlers.ts index fd24b2f..fbb8a61 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerHandlers.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerHandlers.ts @@ -242,7 +242,10 @@ export const useSchedulerHandlers = ({ * Handle modal save (create or update event). */ const handleModalSave = useCallback( - async (event: IcsEvent, targetCalendarUrl: string) => { + async ( + event: IcsEvent, + targetCalendarUrl: string, + ) => { if (modalState.mode === "create") { // Create new event const result = await caldavService.createEvent({ @@ -383,7 +386,7 @@ export const useSchedulerHandlers = ({ } } else { // Option 3: Delete all occurrences OR non-recurring event - const result = await caldavService.deleteEvent(modalState.eventUrl); + const result = await caldavService.deleteEvent(modalState.eventUrl, modalState.etag); if (!result.success) { throw new Error(result.error || "Failed to delete event"); diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/types.ts b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/types.ts index fcfe610..5d58b8f 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/types.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/types.ts @@ -29,7 +29,10 @@ export interface EventModalProps { calendarUrl: string; calendars: CalDavCalendar[]; adapter: EventCalendarAdapter; - onSave: (event: IcsEvent, calendarUrl: string) => Promise; + onSave: ( + event: IcsEvent, + calendarUrl: string, + ) => Promise; onDelete?: ( event: IcsEvent, calendarUrl: string, @@ -79,6 +82,7 @@ export type EventFormSectionId = | "description" | "recurrence" | "attendees" + | "resources" | "videoConference"; /** diff --git a/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts b/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts index 8b1c669..76120e1 100644 --- a/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts +++ b/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts @@ -5,87 +5,87 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { - createSubscriptionToken, - deleteSubscriptionToken, - getSubscriptionToken, - GetSubscriptionTokenResult, - importEventsApi, + Channel, + ChannelError, + createICalFeedChannel, + deleteChannel, + getICalFeedChannel, + GetICalFeedResult, + startImportTask, + pollImportTask, ImportEventsResult, - SubscriptionToken, - SubscriptionTokenError, - SubscriptionTokenParams, } from "../api"; /** - * Result type for useSubscriptionToken hook. + * Result type for useICalFeedChannel hook. */ -export interface UseSubscriptionTokenResult { - token: SubscriptionToken | null; - tokenError: SubscriptionTokenError | null; +export interface UseICalFeedChannelResult { + channel: Channel | null; + channelError: ChannelError | null; isLoading: boolean; refetch: () => void; } /** - * Hook to get subscription token for a calendar by CalDAV path. - * Handles the result/error pattern from getSubscriptionToken. + * Hook to get the ical-feed channel for a calendar by CalDAV path. */ -export const useSubscriptionToken = (caldavPath: string): UseSubscriptionTokenResult => { - const query = useQuery({ - queryKey: ["subscription-token", caldavPath], - queryFn: () => getSubscriptionToken(caldavPath), +export const useICalFeedChannel = ( + caldavPath: string, +): UseICalFeedChannelResult => { + const query = useQuery({ + queryKey: ["ical-feed-channel", caldavPath], + queryFn: () => getICalFeedChannel(caldavPath), enabled: !!caldavPath, retry: false, }); - // Extract token and error from the result using proper type narrowing - const result = query.data; - let token: SubscriptionToken | null = null; - let tokenError: SubscriptionTokenError | null = null; + let channel: Channel | null = null; + let channelError: ChannelError | null = null; + const result = query.data; if (result) { if (result.success) { - token = result.token; + channel = result.channel; } else { - tokenError = result.error; + channelError = result.error; } } return { - token, - tokenError, + channel, + channelError, isLoading: query.isLoading, refetch: query.refetch, }; }; /** - * Hook to create a subscription token. + * Hook to create an ical-feed channel. */ -export const useCreateSubscriptionToken = () => { +export const useCreateICalFeedChannel = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: createSubscriptionToken, - onSuccess: (_data, params: SubscriptionTokenParams) => { - queryClient.invalidateQueries({ - queryKey: ["subscription-token", params.caldavPath], + mutationFn: createICalFeedChannel, + onSuccess: (_data, params) => { + void queryClient.invalidateQueries({ + queryKey: ["ical-feed-channel", params.caldavPath], }); }, }); }; /** - * Hook to delete (revoke) a subscription token. + * Hook to delete a channel by ID. Invalidates ical-feed queries. */ -export const useDeleteSubscriptionToken = () => { +export const useDeleteICalFeedChannel = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: deleteSubscriptionToken, - onSuccess: (_data, caldavPath: string) => { - queryClient.invalidateQueries({ - queryKey: ["subscription-token", caldavPath], + mutationFn: deleteChannel, + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ["ical-feed-channel"], }); }, }); @@ -100,6 +100,9 @@ export const useImportEvents = () => { Error, { caldavPath: string; file: File } >({ - mutationFn: ({ caldavPath, file }) => importEventsApi(caldavPath, file), + mutationFn: async ({ caldavPath, file }) => { + const taskId = await startImportTask(caldavPath, file); + return pollImportTask(taskId); + }, }); }; diff --git a/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts b/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts index 4b19469..a32afe8 100644 --- a/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts +++ b/src/frontend/apps/calendars/src/features/calendar/services/dav/CalDavService.ts @@ -510,6 +510,7 @@ export class CalDavService { if (existingIndex >= 0 && icsCalendar.events) { const updatedEvent = { ...params.event } updatedEvent.sequence = (updatedEvent.sequence ?? 0) + 1 + icsCalendar.events = [...icsCalendar.events] icsCalendar.events[existingIndex] = updatedEvent } else { icsCalendar.events = [params.event] @@ -546,21 +547,26 @@ export class CalDavService { }, 'Failed to update event') } - async deleteEvent(eventUrl: string): Promise { + async deleteEvent(eventUrl: string, etag?: string): Promise { const cachedEvent = this._events.get(eventUrl) const calendarUrl = cachedEvent?.calendarUrl ?? getCalendarUrlFromEventUrl(eventUrl) const calendar = this._calendars.get(calendarUrl) return withErrorHandling(async () => { + const resolvedEtag = etag ?? cachedEvent?.etag + const davObject: DAVCalendarObject = { url: eventUrl, - etag: cachedEvent?.etag, + etag: resolvedEtag, data: cachedEvent?.data ? generateIcsCalendar(cachedEvent.data) : '', } const response = await davDeleteCalendarObject({ calendarObject: davObject, - headers: calendar?.headers, + headers: { + ...(resolvedEtag ? { 'If-Match': resolvedEtag } : {}), + ...calendar?.headers, + }, fetchOptions: calendar?.fetchOptions, }) @@ -769,6 +775,7 @@ export class CalDavService { role: att.role, partstat: att.partstat ?? 'NEEDS-ACTION', rsvp: att.rsvp, + cutype: att.cutype, })) const icsCalendar: IcsCalendar = { diff --git a/src/frontend/apps/calendars/src/features/calendar/services/dav/EventCalendarAdapter.ts b/src/frontend/apps/calendars/src/features/calendar/services/dav/EventCalendarAdapter.ts index 116cce1..282318e 100644 --- a/src/frontend/apps/calendars/src/features/calendar/services/dav/EventCalendarAdapter.ts +++ b/src/frontend/apps/calendars/src/features/calendar/services/dav/EventCalendarAdapter.ts @@ -36,6 +36,7 @@ type ExtendedAttendee = { role?: IcsAttendee['role'] status?: string rsvp?: boolean + cutype?: string } type ExtendedOrganizer = { @@ -245,6 +246,7 @@ export class EventCalendarAdapter { role: att.role as ExtendedAttendee['role'], status: att.partstat as ExtendedAttendee['status'], rsvp: att.rsvp, + cutype: att.cutype, }))), categories: icsEvent.categories, priority: icsEvent.priority != null ? Number(icsEvent.priority) : undefined, @@ -416,6 +418,7 @@ export class EventCalendarAdapter { role: att.role, partstat: (att.status as IcsAttendee['partstat']) ?? 'NEEDS-ACTION', rsvp: att.rsvp, + cutype: att.cutype, })) } diff --git a/src/frontend/apps/calendars/src/features/calendar/utils/DavClient.ts b/src/frontend/apps/calendars/src/features/calendar/utils/DavClient.ts index 6b56d74..5920e8f 100644 --- a/src/frontend/apps/calendars/src/features/calendar/utils/DavClient.ts +++ b/src/frontend/apps/calendars/src/features/calendar/utils/DavClient.ts @@ -7,7 +7,7 @@ import { getOrigin } from "@/features/api/utils"; -export const caldavServerUrl = `${getOrigin()}/api/v1.0/caldav/`; +export const caldavServerUrl = `${getOrigin()}/caldav/`; export const headers = { "Content-Type": "application/xml", diff --git a/src/frontend/apps/calendars/src/features/i18n/translations.json b/src/frontend/apps/calendars/src/features/i18n/translations.json index 83d82e2..935a17b 100644 --- a/src/frontend/apps/calendars/src/features/i18n/translations.json +++ b/src/frontend/apps/calendars/src/features/i18n/translations.json @@ -15,7 +15,8 @@ "button": "Logout" }, "common": { - "cancel": "Cancel" + "cancel": "Cancel", + "close": "Close" }, "clipboard": { "success": "Copied to clipboard", @@ -27,6 +28,7 @@ "logout": "Logout", "login": "Login", "my_account": "My Account", + "settings": "Settings", "api": { "error": { "unexpected": "An unexpected error occurred." @@ -150,6 +152,7 @@ "removeVisio": "Remove video conference", "videoLink": "Video conference link", "addAttendees": "Add participants", + "addResources": "Add resources", "addDescription": "Add description", "addReminder": "Add reminder", "addAttachment": "Add attachment", @@ -330,6 +333,10 @@ "viewProfile": "View profile", "cannotRemoveOrganizer": "Cannot remove organizer" }, + "resources": { + "placeholder": "Select a resource...", + "remove": "Remove resource" + }, "weekdaysFull": { "monday": "Monday", "tuesday": "Tuesday", @@ -430,7 +437,82 @@ "eventNotFound": "The event was not found. It may have been deleted.", "eventPast": "This event has already passed.", "notAttendee": "You are not listed as an attendee of this event.", - "updateFailed": "An error occurred while updating. Please try again." + "updateFailed": "An error occurred while updating. Please try again.", + "tokenExpired": "This link has expired. Please ask the organizer for a new invitation.", + "rateLimited": "Too many requests. Please wait a moment and try again." + } + }, + "resources": { + "title": "Resources", + "description": "Manage meeting rooms and shared resources.", + "search": "Search resources...", + "loading": "Loading resources...", + "empty": "No resources have been created yet.", + "noResults": "No resources match your search.", + "types": { + "room": "Room", + "resource": "Resource" + }, + "filters": { + "all": "All" + }, + "create": { + "button": "Add resource", + "title": "Create resource", + "nameLabel": "Name", + "typeLabel": "Type", + "submit": "Create", + "success": "Resource created successfully.", + "error": "Failed to create resource." + }, + "delete": { + "button": "Delete", + "title": "Delete resource", + "message": "Are you sure you want to delete \"{{name}}\"? This cannot be undone.", + "confirm": "Delete", + "success": "Resource deleted successfully.", + "error": "Failed to delete resource." + } + }, + "integrations": { + "title": "Integrations", + "description": "Manage integration channels for external services to access your calendars.", + "loading": "Loading integrations...", + "empty": "No integrations have been created yet.", + "roles": { + "reader": "Reader", + "editor": "Editor", + "admin": "Admin" + }, + "lastUsed": "Last used: {{date}}", + "create": { + "button": "Add integration", + "title": "Create integration", + "nameLabel": "Name", + "roleLabel": "Role", + "submit": "Create", + "success": "Integration created successfully.", + "error": "Failed to create integration.", + "tokenTitle": "Integration token", + "tokenWarning": "Copy this token now. It will not be shown again." + }, + "delete": { + "button": "Delete", + "title": "Delete integration", + "message": "Are you sure you want to delete \"{{name}}\"? This cannot be undone.", + "confirm": "Delete", + "success": "Integration deleted successfully.", + "error": "Failed to delete integration." + }, + "regenerate": { + "button": "Regenerate token", + "success": "Token regenerated successfully.", + "error": "Failed to regenerate token.", + "tokenWarning": "Copy this new token now. It will not be shown again." + }, + "token": { + "copy": "Copy token", + "copied": "Token copied to clipboard." } } } @@ -490,7 +572,8 @@ } }, "common": { - "cancel": "Annuler" + "cancel": "Annuler", + "close": "Fermer" }, "clipboard": { "success": "Copié dans le presse-papiers", @@ -502,6 +585,7 @@ "logout": "Déconnexion", "login": "Connexion", "my_account": "Mon Compte", + "settings": "Paramètres", "api": { "error": { "unexpected": "Une erreur inattendue est survenue." @@ -889,6 +973,7 @@ "removeVisio": "Supprimer la visioconférence", "videoLink": "Lien de visioconférence", "addAttendees": "Ajouter des participants", + "addResources": "Ajouter des ressources", "addDescription": "Ajouter une description", "addReminder": "Ajouter un rappel", "addAttachment": "Ajouter une pièce jointe", @@ -1069,6 +1154,10 @@ "viewProfile": "Voir le profil", "cannotRemoveOrganizer": "Impossible de retirer l'organisateur" }, + "resources": { + "placeholder": "Sélectionner une ressource...", + "remove": "Retirer la ressource" + }, "weekdaysFull": { "monday": "lundi", "tuesday": "mardi", @@ -1169,7 +1258,82 @@ "eventNotFound": "L'événement n'a pas été trouvé. Il a peut-être été supprimé.", "eventPast": "Cet événement est déjà passé.", "notAttendee": "Vous ne figurez pas parmi les participants de cet événement.", - "updateFailed": "Une erreur est survenue lors de la mise à jour. Veuillez réessayer." + "updateFailed": "Une erreur est survenue lors de la mise à jour. Veuillez réessayer.", + "tokenExpired": "Ce lien a expiré. Veuillez demander une nouvelle invitation à l'organisateur.", + "rateLimited": "Trop de requêtes. Veuillez patienter un instant et réessayer." + } + }, + "resources": { + "title": "Ressources", + "description": "Gérer les salles de réunion et les ressources partagées.", + "search": "Rechercher des ressources...", + "loading": "Chargement des ressources...", + "empty": "Aucune ressource n'a encore été créée.", + "noResults": "Aucune ressource ne correspond à votre recherche.", + "types": { + "room": "Salle", + "resource": "Ressource" + }, + "filters": { + "all": "Toutes" + }, + "create": { + "button": "Ajouter une ressource", + "title": "Créer une ressource", + "nameLabel": "Nom", + "typeLabel": "Type", + "submit": "Créer", + "success": "Ressource créée avec succès.", + "error": "Échec de la création de la ressource." + }, + "delete": { + "button": "Supprimer", + "title": "Supprimer la ressource", + "message": "Êtes-vous sûr de vouloir supprimer « {{name}} » ? Cette action est irréversible.", + "confirm": "Supprimer", + "success": "Ressource supprimée avec succès.", + "error": "Échec de la suppression de la ressource." + } + }, + "integrations": { + "title": "Intégrations", + "description": "Gérer les canaux d'intégration pour que les services externes puissent accéder à vos agendas.", + "loading": "Chargement des intégrations...", + "empty": "Aucune intégration n'a encore été créée.", + "roles": { + "reader": "Lecteur", + "editor": "Éditeur", + "admin": "Admin" + }, + "lastUsed": "Dernière utilisation : {{date}}", + "create": { + "button": "Ajouter une intégration", + "title": "Créer une intégration", + "nameLabel": "Nom", + "roleLabel": "Rôle", + "submit": "Créer", + "success": "Intégration créée avec succès.", + "error": "Échec de la création de l'intégration.", + "tokenTitle": "Jeton d'intégration", + "tokenWarning": "Copiez ce jeton maintenant. Il ne sera plus affiché." + }, + "delete": { + "button": "Supprimer", + "title": "Supprimer l'intégration", + "message": "Êtes-vous sûr de vouloir supprimer « {{name}} » ? Cette action est irréversible.", + "confirm": "Supprimer", + "success": "Intégration supprimée avec succès.", + "error": "Échec de la suppression de l'intégration." + }, + "regenerate": { + "button": "Regénérer le jeton", + "success": "Jeton regénéré avec succès.", + "error": "Échec de la regénération du jeton.", + "tokenWarning": "Copiez ce nouveau jeton maintenant. Il ne sera plus affiché." + }, + "token": { + "copy": "Copier le jeton", + "copied": "Jeton copié dans le presse-papiers." } } } @@ -1224,7 +1388,8 @@ } }, "common": { - "cancel": "Annuleren" + "cancel": "Annuleren", + "close": "Sluiten" }, "clipboard": { "success": "Gekopieërd naar klembord", @@ -1236,6 +1401,7 @@ "logout": "Uitloggen", "login": "Inloggen", "my_account": "Mijn account", + "settings": "Instellingen", "api": { "error": { "unexpected": "Er is een onverwachte fout opgetreden." @@ -1370,6 +1536,7 @@ "removeVisio": "Videoconferentie verwijderen", "videoLink": "Videoconferentie link", "addAttendees": "Deelnemers toevoegen", + "addResources": "Middelen toevoegen", "addDescription": "Beschrijving toevoegen", "addReminder": "Herinnering toevoegen", "addAttachment": "Bijlage toevoegen", @@ -1550,6 +1717,10 @@ "viewProfile": "Profiel bekijken", "cannotRemoveOrganizer": "Kan organisator niet verwijderen" }, + "resources": { + "placeholder": "Selecteer een middel...", + "remove": "Middel verwijderen" + }, "weekdaysFull": { "monday": "maandag", "tuesday": "dinsdag", @@ -1650,7 +1821,82 @@ "eventNotFound": "Het evenement is niet gevonden. Het is mogelijk verwijderd.", "eventPast": "Dit evenement is al voorbij.", "notAttendee": "U staat niet vermeld als deelnemer van dit evenement.", - "updateFailed": "Er is een fout opgetreden bij het bijwerken. Probeer het opnieuw." + "updateFailed": "Er is een fout opgetreden bij het bijwerken. Probeer het opnieuw.", + "tokenExpired": "Deze link is verlopen. Vraag de organisator om een nieuwe uitnodiging.", + "rateLimited": "Te veel verzoeken. Wacht even en probeer het opnieuw." + } + }, + "resources": { + "title": "Middelen", + "description": "Vergaderruimtes en gedeelde middelen beheren.", + "search": "Middelen zoeken...", + "loading": "Middelen laden...", + "empty": "Er zijn nog geen middelen aangemaakt.", + "noResults": "Geen middelen gevonden.", + "types": { + "room": "Ruimte", + "resource": "Middel" + }, + "filters": { + "all": "Alle" + }, + "create": { + "button": "Middel toevoegen", + "title": "Middel aanmaken", + "nameLabel": "Naam", + "typeLabel": "Type", + "submit": "Aanmaken", + "success": "Middel succesvol aangemaakt.", + "error": "Middel aanmaken mislukt." + }, + "delete": { + "button": "Verwijderen", + "title": "Middel verwijderen", + "message": "Weet u zeker dat u \"{{name}}\" wilt verwijderen? Dit kan niet ongedaan worden gemaakt.", + "confirm": "Verwijderen", + "success": "Middel succesvol verwijderd.", + "error": "Middel verwijderen mislukt." + } + }, + "integrations": { + "title": "Integraties", + "description": "Beheer integratiekanalen zodat externe diensten toegang hebben tot uw agenda's.", + "loading": "Integraties laden...", + "empty": "Er zijn nog geen integraties aangemaakt.", + "roles": { + "reader": "Lezer", + "editor": "Bewerker", + "admin": "Admin" + }, + "lastUsed": "Laatst gebruikt: {{date}}", + "create": { + "button": "Integratie toevoegen", + "title": "Integratie aanmaken", + "nameLabel": "Naam", + "roleLabel": "Rol", + "submit": "Aanmaken", + "success": "Integratie succesvol aangemaakt.", + "error": "Integratie aanmaken mislukt.", + "tokenTitle": "Integratietoken", + "tokenWarning": "Kopieer dit token nu. Het wordt niet meer getoond." + }, + "delete": { + "button": "Verwijderen", + "title": "Integratie verwijderen", + "message": "Weet u zeker dat u \"{{name}}\" wilt verwijderen? Dit kan niet ongedaan worden gemaakt.", + "confirm": "Verwijderen", + "success": "Integratie succesvol verwijderd.", + "error": "Integratie verwijderen mislukt." + }, + "regenerate": { + "button": "Token opnieuw genereren", + "success": "Token succesvol opnieuw gegenereerd.", + "error": "Token opnieuw genereren mislukt.", + "tokenWarning": "Kopieer dit nieuwe token nu. Het wordt niet meer getoond." + }, + "token": { + "copy": "Token kopiëren", + "copied": "Token gekopieerd naar klembord." } } } diff --git a/src/frontend/apps/calendars/src/features/integrations/api/useChannels.ts b/src/frontend/apps/calendars/src/features/integrations/api/useChannels.ts new file mode 100644 index 0000000..e22ed59 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/integrations/api/useChannels.ts @@ -0,0 +1,83 @@ +import { + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { fetchAPI } from "@/features/api/fetchApi"; +import type { + Channel, + ChannelCreateRequest, + ChannelWithToken, +} from "../types"; + +const CHANNELS_QUERY_KEY = ["channels"]; + +async function fetchChannels(): Promise { + const response = await fetchAPI("channels/"); + return response.json(); +} + +async function createChannel( + data: ChannelCreateRequest, +): Promise { + const response = await fetchAPI("channels/", { + method: "POST", + body: JSON.stringify(data), + }); + return response.json(); +} + +async function deleteChannel(id: string): Promise { + await fetchAPI(`channels/${id}/`, { + method: "DELETE", + }); +} + +async function regenerateToken( + id: string, +): Promise<{ token: string }> { + const response = await fetchAPI( + `channels/${id}/regenerate-token/`, + { method: "POST" }, + ); + return response.json(); +} + +export const useChannels = () => { + return useQuery({ + queryKey: CHANNELS_QUERY_KEY, + queryFn: fetchChannels, + }); +}; + +export const useCreateChannel = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createChannel, + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: CHANNELS_QUERY_KEY, + }); + }, + }); +}; + +export const useDeleteChannel = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteChannel, + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: CHANNELS_QUERY_KEY, + }); + }, + }); +}; + +export const useRegenerateToken = () => { + return useMutation({ + mutationFn: regenerateToken, + }); +}; diff --git a/src/frontend/apps/calendars/src/features/integrations/components/ChannelCard.tsx b/src/frontend/apps/calendars/src/features/integrations/components/ChannelCard.tsx new file mode 100644 index 0000000..634e472 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/integrations/components/ChannelCard.tsx @@ -0,0 +1,68 @@ +import { useTranslation } from "react-i18next"; +import { Button } from "@gouvfr-lasuite/cunningham-react"; + +import type { Channel } from "../types"; + +type ChannelCardProps = { + channel: Channel; + onDelete: (id: string) => void; + onRegenerate: (id: string) => void; +}; + +export const ChannelCard = ({ + channel, + onDelete, + onRegenerate, +}: ChannelCardProps) => { + const { t } = useTranslation(); + + return ( +
+
+ key +
+
+
{channel.name}
+
+ + {t(`integrations.roles.${channel.role}`)} + + {channel.caldav_path && ( + + {channel.caldav_path} + + )} + {channel.last_used_at && ( + + {t("integrations.lastUsed", { + date: new Date( + channel.last_used_at, + ).toLocaleDateString(), + })} + + )} +
+
+
+
+
+ ); +}; diff --git a/src/frontend/apps/calendars/src/features/integrations/components/ChannelList.tsx b/src/frontend/apps/calendars/src/features/integrations/components/ChannelList.tsx new file mode 100644 index 0000000..4e7d543 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/integrations/components/ChannelList.tsx @@ -0,0 +1,156 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, useModal } from "@gouvfr-lasuite/cunningham-react"; +import { useRouter } from "next/router"; + +import { + addToast, + ToasterItem, +} from "@/features/ui/components/toaster/Toaster"; +import { useChannels, useRegenerateToken } from "../api/useChannels"; +import { ChannelCard } from "./ChannelCard"; +import { CreateChannelModal } from "./CreateChannelModal"; +import { DeleteChannelModal } from "./DeleteChannelModal"; +import { TokenRevealBox } from "./TokenRevealBox"; + +export const ChannelList = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { data: channels, isLoading } = useChannels(); + const regenerateToken = useRegenerateToken(); + const createModal = useModal(); + + const [deleteTarget, setDeleteTarget] = useState<{ + id: string; + name: string; + } | null>(null); + + const [regeneratedToken, setRegeneratedToken] = useState< + string | null + >(null); + + const handleRegenerate = async (id: string) => { + try { + const result = await regenerateToken.mutateAsync(id); + setRegeneratedToken(result.token); + addToast( + + + {t("integrations.regenerate.success")} + + , + ); + } catch { + addToast( + + + {t("integrations.regenerate.error")} + + , + ); + } + }; + + return ( +
+
+
+
+ +
+ +

+ {t("integrations.description")} +

+ + {regeneratedToken && ( +
+

{t("integrations.regenerate.tokenWarning")}

+ + +
+ )} + + {isLoading ? ( +
+ + hourglass_empty + +

{t("integrations.loading")}

+
+ ) : !channels || channels.length === 0 ? ( +
+ + integration_instructions + +

{t("integrations.empty")}

+
+ ) : ( +
+ {channels.map((channel) => ( + { + const ch = channels.find( + (c) => c.id === id, + ); + if (ch) { + setDeleteTarget({ + id, + name: ch.name, + }); + } + }} + onRegenerate={(id) => + void handleRegenerate(id) + } + /> + ))} +
+ )} + + {createModal.isOpen && ( + + )} + + {deleteTarget && ( + setDeleteTarget(null)} + /> + )} +
+ ); +}; diff --git a/src/frontend/apps/calendars/src/features/integrations/components/CreateChannelModal.tsx b/src/frontend/apps/calendars/src/features/integrations/components/CreateChannelModal.tsx new file mode 100644 index 0000000..0f5e466 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/integrations/components/CreateChannelModal.tsx @@ -0,0 +1,153 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Button, + Input, + Modal, + ModalSize, + Select, +} from "@gouvfr-lasuite/cunningham-react"; + +import { + addToast, + ToasterItem, +} from "@/features/ui/components/toaster/Toaster"; +import { useCreateChannel } from "../api/useChannels"; +import type { ChannelRole } from "../types"; +import { TokenRevealBox } from "./TokenRevealBox"; + +type CreateChannelModalProps = { + isOpen: boolean; + onClose: () => void; +}; + +export const CreateChannelModal = ({ + isOpen, + onClose, +}: CreateChannelModalProps) => { + const { t } = useTranslation(); + const createChannel = useCreateChannel(); + + const [name, setName] = useState(""); + const [role, setRole] = useState("reader"); + const [revealedToken, setRevealedToken] = useState< + string | null + >(null); + + const canSubmit = + name.trim().length > 0 && !createChannel.isPending; + + const handleSubmit = async () => { + if (!canSubmit) return; + + try { + const result = await createChannel.mutateAsync({ + name: name.trim(), + role, + }); + setRevealedToken(result.token); + addToast( + + {t("integrations.create.success")} + , + ); + } catch { + addToast( + + {t("integrations.create.error")} + , + ); + } + }; + + const handleClose = () => { + setName(""); + setRole("reader"); + setRevealedToken(null); + onClose(); + }; + + const roleOptions = [ + { + label: t("integrations.roles.reader"), + value: "reader", + }, + { + label: t("integrations.roles.editor"), + value: "editor", + }, + { + label: t("integrations.roles.admin"), + value: "admin", + }, + ]; + + if (revealedToken) { + return ( + + {t("common.close")} + + } + > +
+

{t("integrations.create.tokenWarning")}

+ +
+
+ ); + } + + return ( + + + + + } + > +
+ + setName( + (e.target as HTMLInputElement).value, + ) + } + fullWidth + /> + + setName( + (e.target as HTMLInputElement).value, + ) + } + fullWidth + /> + + setSearch( + (e.target as HTMLInputElement).value, + ) + } + fullWidth + /> +
+ + + +
+
+ + {isLoading ? ( +
+ + hourglass_empty + +

{t("resources.loading")}

+
+ ) : filtered.length === 0 ? ( +
+ meeting_room +

+ {search || typeFilter !== "ALL" + ? t("resources.noResults") + : t("resources.empty")} +

+
+ ) : ( +
+ {filtered.map((resource) => ( + + ))} +
+ )} + + {createModal.isOpen && ( + + )} + + {deleteTarget && ( + setDeleteTarget(null)} + onSuccess={onRefresh} + /> + )} +
+ ); +}; diff --git a/src/frontend/apps/calendars/src/features/resources/components/Resources.scss b/src/frontend/apps/calendars/src/features/resources/components/Resources.scss new file mode 100644 index 0000000..f04b34c --- /dev/null +++ b/src/frontend/apps/calendars/src/features/resources/components/Resources.scss @@ -0,0 +1,131 @@ +.resources-page { + max-width: 900px; + margin: 0 auto; + padding: var(--c--globals--spacings--base); +} + +.resource-list { + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--c--globals--spacings--base); + + h2 { + margin: 0; + font-size: 1.5rem; + } + } + + &__title-row { + display: flex; + align-items: center; + gap: var(--c--globals--spacings--xs); + } + + &__filters { + display: flex; + flex-direction: column; + gap: var(--c--globals--spacings--sm); + margin-bottom: var(--c--globals--spacings--base); + } + + &__type-filters { + display: flex; + gap: var(--c--globals--spacings--2xs); + } + + &__grid { + display: flex; + flex-direction: column; + gap: var(--c--globals--spacings--sm); + } + + &__empty, + &__loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--c--globals--spacings--xl) 0; + color: var(--c--contextuals--content--semantic--neutral--tertiary); + + .material-icons { + font-size: 48px; + margin-bottom: var(--c--globals--spacings--sm); + } + } + + &__spinner { + animation: spin 1.5s linear infinite; + } +} + +.resource-card { + display: flex; + align-items: center; + gap: var(--c--globals--spacings--sm); + padding: var(--c--globals--spacings--sm); + border: 1px solid var(--c--contextuals--border--semantic--neutral--secondary); + border-radius: 8px; + transition: box-shadow 0.15s ease; + + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 8px; + background: var(--c--contextuals--background--semantic--brand--primary); + color: white; + flex-shrink: 0; + + .material-icons { + font-size: 20px; + } + } + + &__info { + flex: 1; + min-width: 0; + } + + &__name { + font-weight: 600; + font-size: 0.95rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__meta { + display: flex; + gap: var(--c--globals--spacings--xs); + font-size: 0.8rem; + color: var(--c--contextuals--content--semantic--neutral--tertiary); + } + + &__actions { + flex-shrink: 0; + } +} + +.resources-create-modal { + display: flex; + flex-direction: column; + gap: var(--c--globals--spacings--sm); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/frontend/apps/calendars/src/features/resources/types.ts b/src/frontend/apps/calendars/src/features/resources/types.ts new file mode 100644 index 0000000..161743c --- /dev/null +++ b/src/frontend/apps/calendars/src/features/resources/types.ts @@ -0,0 +1,15 @@ +export type ResourceType = "ROOM" | "RESOURCE"; + +export type Resource = { + id: string; + name: string; + email: string; + resource_type: ResourceType; + principal_uri: string; + calendar_uri: string; +}; + +export type ResourceCreateRequest = { + name: string; + resource_type: ResourceType; +}; diff --git a/src/frontend/apps/calendars/src/features/ui/components/logo/DynamicCalendarLogo.scss b/src/frontend/apps/calendars/src/features/ui/components/logo/DynamicCalendarLogo.scss index 5674b37..2e7f164 100644 --- a/src/frontend/apps/calendars/src/features/ui/components/logo/DynamicCalendarLogo.scss +++ b/src/frontend/apps/calendars/src/features/ui/components/logo/DynamicCalendarLogo.scss @@ -43,7 +43,7 @@ .calendars__dynamic-logo__day { left: 50%; - top: 50%; + top: 35%; transform: translate(-50%, 15%); } } diff --git a/src/frontend/apps/calendars/src/pages/calendar.tsx b/src/frontend/apps/calendars/src/pages/calendar.tsx index 3c35594..2757810 100644 --- a/src/frontend/apps/calendars/src/pages/calendar.tsx +++ b/src/frontend/apps/calendars/src/pages/calendar.tsx @@ -2,6 +2,8 @@ * Calendar page - Main calendar view with sidebar. */ +import { useEffect } from "react"; + import { MainLayout } from "@gouvfr-lasuite/ui-kit"; import Head from "next/head"; import { useTranslation } from "next-i18next"; @@ -9,37 +11,35 @@ import { useTranslation } from "next-i18next"; import { login, useAuth } from "@/features/auth/Auth"; import { LeftPanel } from "@/features/calendar/components"; import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout"; -import { HeaderRight } from "@/features/layouts/components/header/Header"; +import { + HeaderIcon, + HeaderRight, +} from "@/features/layouts/components/header/Header"; import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage"; import { Toaster } from "@/features/ui/components/toaster/Toaster"; import { Scheduler } from "@/features/calendar/components/scheduler/Scheduler"; import { CalendarContextProvider } from "@/features/calendar/contexts"; -import { DynamicCalendarLogo } from "@/features/ui/components/logo"; export default function CalendarPage() { const { t } = useTranslation(); const { user } = useAuth(); - // Redirect to login if not authenticated - if (!user) { - if (typeof window !== "undefined") { + useEffect(() => { + if (!user) { login(window.location.href); - } - return ; - } - - // Redirect to no-access if not entitled - if (user.can_access === false) { - if (typeof window !== "undefined") { + } else if (user.can_access === false) { window.location.href = "/no-access"; } + }, [user]); + + if (!user || user.can_access === false) { return ; } return ( <> - Calendrier - {t("app_title")} + {t("app_title")} @@ -64,11 +64,7 @@ CalendarPage.getLayout = function getLayout(page: React.ReactElement) { } - icon={ -
- -
- } + icon={} rightHeaderContent={} > {page} diff --git a/src/frontend/apps/calendars/src/pages/index.tsx b/src/frontend/apps/calendars/src/pages/index.tsx index fc413f6..b28cadf 100644 --- a/src/frontend/apps/calendars/src/pages/index.tsx +++ b/src/frontend/apps/calendars/src/pages/index.tsx @@ -4,8 +4,11 @@ import { useTranslation } from "next-i18next"; import { Hero, Footer, MainLayout, HomeGutter } from "@gouvfr-lasuite/ui-kit"; import { login, useAuth } from "@/features/auth/Auth"; import { useEffect } from "react"; -import banner from "@/assets/home/banner.png"; -import { HeaderRight } from "@/features/layouts/components/header/Header"; +import banner from "@/assets/home/banner.svg"; +import { + HeaderIcon, + HeaderRight, +} from "@/features/layouts/components/header/Header"; import { addToast, Toaster, @@ -16,7 +19,6 @@ import { useConfig } from "@/features/config/ConfigProvider"; import { LeftPanelMobile } from "@/features/layouts/components/left-panel/LeftPanelMobile"; import { SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL } from "@/features/api/fetchApi"; import { useThemeCustomization } from "@/hooks/useThemeCustomization"; -import { Feedback } from "@/features/feedback/Feedback"; import { DynamicCalendarLogo } from "@/features/ui/components/logo"; export default function HomePage() { const { t } = useTranslation(); @@ -36,7 +38,16 @@ export default function HomePage() { ); if (attemptedUrl) { sessionStorage.removeItem(SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL); - window.location.href = attemptedUrl; + try { + const url = new URL(attemptedUrl, window.location.origin); + if (url.origin === window.location.origin) { + window.location.href = url.href; + } else { + window.location.href = "/calendar"; + } + } catch { + window.location.href = "/calendar"; + } } else { // Redirect authenticated users to calendar page window.location.href = "/calendar"; @@ -112,12 +123,7 @@ HomePage.getLayout = function getLayout(page: React.ReactElement) { enableResize hideLeftPanelOnDesktop={true} leftPanelContent={} - icon={ -
- - -
- } + icon={} rightHeaderContent={} > {page} diff --git a/src/frontend/apps/calendars/src/pages/integrations.tsx b/src/frontend/apps/calendars/src/pages/integrations.tsx new file mode 100644 index 0000000..79e340e --- /dev/null +++ b/src/frontend/apps/calendars/src/pages/integrations.tsx @@ -0,0 +1,74 @@ +import { useEffect } from "react"; + +import { MainLayout } from "@gouvfr-lasuite/ui-kit"; +import Head from "next/head"; +import { useTranslation } from "next-i18next"; + +import { login, useAuth } from "@/features/auth/Auth"; +import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout"; +import { + HeaderIcon, + HeaderRight, +} from "@/features/layouts/components/header/Header"; +import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage"; +import { Toaster } from "@/features/ui/components/toaster/Toaster"; +import { ChannelList } from "@/features/integrations/components/ChannelList"; + +export default function IntegrationsPage() { + const { t } = useTranslation(); + const { user } = useAuth(); + + useEffect(() => { + if (!user) { + login(window.location.href); + } else if (user.can_access === false) { + window.location.href = "/no-access"; + } + }, [user]); + + if (!user || user.can_access === false) { + return ; + } + + return ( + <> + + + {t("integrations.title")} - {t("app_title")} + + + + + + +
+ +
+ + + + ); +} + +IntegrationsPage.getLayout = function getLayout( + page: React.ReactElement, +) { + return ( + + } + rightHeaderContent={} + > + {page} + + + ); +}; diff --git a/src/frontend/apps/calendars/src/pages/no-access.tsx b/src/frontend/apps/calendars/src/pages/no-access.tsx index 2148aec..6bfdc46 100644 --- a/src/frontend/apps/calendars/src/pages/no-access.tsx +++ b/src/frontend/apps/calendars/src/pages/no-access.tsx @@ -4,9 +4,11 @@ import { useTranslation } from "react-i18next"; import { logout } from "@/features/auth/Auth"; import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout"; -import { HeaderRight } from "@/features/layouts/components/header/Header"; +import { + HeaderIcon, + HeaderRight, +} from "@/features/layouts/components/header/Header"; import { GenericDisclaimer } from "@/features/ui/components/generic-disclaimer/GenericDisclaimer"; -import { DynamicCalendarLogo } from "@/features/ui/components/logo"; export default function NoAccessPage() { const { t } = useTranslation(); @@ -27,11 +29,7 @@ NoAccessPage.getLayout = function getLayout(page: React.ReactElement) { - -
- } + icon={} rightHeaderContent={} > {page} diff --git a/src/frontend/apps/calendars/src/pages/resources.tsx b/src/frontend/apps/calendars/src/pages/resources.tsx new file mode 100644 index 0000000..8c4a473 --- /dev/null +++ b/src/frontend/apps/calendars/src/pages/resources.tsx @@ -0,0 +1,72 @@ +import { useEffect } from "react"; + +import { MainLayout } from "@gouvfr-lasuite/ui-kit"; +import Head from "next/head"; +import { useTranslation } from "next-i18next"; + +import { login, useAuth } from "@/features/auth/Auth"; +import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout"; +import { + HeaderIcon, + HeaderRight, +} from "@/features/layouts/components/header/Header"; +import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage"; +import { Toaster } from "@/features/ui/components/toaster/Toaster"; +import { ResourceList } from "@/features/resources/components/ResourceList"; +import { useResourcePrincipals } from "@/features/resources/api/useResourcePrincipals"; + +export default function ResourcesPage() { + const { t } = useTranslation(); + const { user } = useAuth(); + const { resources, isLoading, refresh } = useResourcePrincipals(); + + useEffect(() => { + if (!user) { + login(window.location.href); + } else if (user.can_access === false) { + window.location.href = "/no-access"; + } + }, [user]); + + if (!user || user.can_access === false) { + return ; + } + + return ( + <> + + + {t("resources.title")} - {t("app_title")} + + + + + + +
+ +
+ + + + ); +} + +ResourcesPage.getLayout = function getLayout(page: React.ReactElement) { + return ( + + } + rightHeaderContent={} + > + {page} + + + ); +}; diff --git a/src/frontend/apps/calendars/src/styles/globals.scss b/src/frontend/apps/calendars/src/styles/globals.scss index b9fe4c5..2c17a4f 100644 --- a/src/frontend/apps/calendars/src/styles/globals.scss +++ b/src/frontend/apps/calendars/src/styles/globals.scss @@ -21,6 +21,7 @@ @use "./../features/calendar/components/scheduler/event-modal-sections/SectionRow.scss"; @use "./../features/calendar/components/scheduler/event-modal-sections/InvitationResponseSection.scss"; @use "./../features/calendar/components/scheduler/event-modal-sections/SectionPill.scss"; +@use "./../features/resources/components/Resources"; @use "./../pages/index.scss" as *; @use "./../pages/calendar.scss" as *; html, diff --git a/src/frontend/apps/calendars/tsconfig.json b/src/frontend/apps/calendars/tsconfig.json index be8dd64..59e8327 100644 --- a/src/frontend/apps/calendars/tsconfig.json +++ b/src/frontend/apps/calendars/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -11,12 +15,21 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, - "include": ["next-env.d.ts", "svg.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "svg.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/src/frontend/apps/e2e/playwright.config.ts b/src/frontend/apps/e2e/playwright.config.ts index 8926e67..0295ac7 100644 --- a/src/frontend/apps/e2e/playwright.config.ts +++ b/src/frontend/apps/e2e/playwright.config.ts @@ -32,7 +32,7 @@ export default defineConfig({ }, webServer: { - command: !process.env.CI ? `cd ../calendars && yarn dev --port ${PORT}` : "", + command: !process.env.CI ? `cd ../calendars && npm run dev -- --port ${PORT}` : "", url: baseURL, timeout: 120 * 1000, reuseExistingServer: true, diff --git a/src/frontend/docker/files/usr/local/bin/entrypoint b/src/frontend/docker/files/usr/local/bin/entrypoint deleted file mode 100755 index 8959ebc..0000000 --- a/src/frontend/docker/files/usr/local/bin/entrypoint +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -e - -exec "$@" diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json new file mode 100644 index 0000000..1e596dc --- /dev/null +++ b/src/frontend/package-lock.json @@ -0,0 +1,14344 @@ +{ + "name": "main", + "version": "0.10.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "main", + "version": "0.10.1", + "workspaces": [ + "apps/*" + ], + "engines": { + "node": ">=24.0.0" + } + }, + "apps/calendars": { + "version": "0.0.1", + "dependencies": { + "@event-calendar/core": "^5.4.1", + "@gouvfr-lasuite/cunningham-react": "4.2.0", + "@gouvfr-lasuite/ui-kit": "0.19.10", + "@tanstack/react-query": "5.90.21", + "@tanstack/react-table": "8.21.3", + "@viselect/react": "3.9.0", + "clsx": "2.1.1", + "date-fns": "4.1.0", + "i18next": "25.8.14", + "i18next-browser-languagedetector": "8.2.1", + "ical.js": "^2.2.1", + "next": "16.1.6", + "next-i18next": "15.4.3", + "pretty-bytes": "7.1.0", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-dropzone": "15.0.0", + "react-hook-form": "7.71.2", + "react-i18next": "16.5.6", + "react-toastify": "11.0.5", + "sass": "1.97.3", + "ts-ics": "^2.4.2", + "tsdav": "2.1.8" + }, + "devDependencies": { + "@gouvfr-lasuite/cunningham-tokens": "3.1.0", + "@tanstack/eslint-plugin-query": "5.91.4", + "@tanstack/react-query-devtools": "5.91.3", + "@types/jest": "30.0.0", + "@types/node": "24.12.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "eslint": "9.30.1", + "eslint-config-next": "16.1.6", + "jest": "30.2.0", + "ts-jest": "29.4.6", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=24.0.0", + "npm": ">=10.0.0" + } + }, + "apps/calendars/node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "apps/e2e": { + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "1.56.1", + "@types/node": "24.10.1" + } + }, + "apps/e2e/node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@event-calendar/core": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@event-calendar/core/-/core-5.4.1.tgz", + "integrity": "sha512-YvB6KyzsZHwZjE5DVEKahGbw46BKEOHIg8jtiqFGTNP/lyd20N1dk7LeYgwDPdvcXAAyhaQHNfdQRKH4LKzqYQ==", + "license": "MIT", + "dependencies": { + "svelte": "^5.53.2" + } + }, + "node_modules/@fontsource-variable/roboto-flex": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource-variable/roboto-flex/-/roboto-flex-5.2.5.tgz", + "integrity": "sha512-yrZ9rWNvfM4IBqJSpoFV4wC1GaKNwpzhihyBke+N3WiA0Y7LYxwj6kvvHgecabVIHFL3ZZJTwj3KcKOCyASFPg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/material-icons": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource/material-icons/-/material-icons-5.2.5.tgz", + "integrity": "sha512-9k0LBRVgResIeD+vC/epYmm/awN2k2L8twwEtUWQ3FHluMi+7PbISOpXqksvfqPn9FJy4/KEeWOhFTR/SrbhNw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/material-icons-outlined": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource/material-icons-outlined/-/material-icons-outlined-5.2.5.tgz", + "integrity": "sha512-soAUWorSKrLN0a7wk74pedV0ZxhLaMD40DuUTMIqTpDcdJ/pxaG+/OcXtMEzx2+5K5FwL7yS525ZInAzPG051Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@gouvfr-lasuite/cunningham-react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@gouvfr-lasuite/cunningham-react/-/cunningham-react-4.2.0.tgz", + "integrity": "sha512-97eTA+v/ySsl6d5NCn6D2mVoNqrJnHWG+PWCY1L/Fz8MEx18sPwqUsqdaIuSj0C+iug0SCJ6Ufx57MURHCf0oA==", + "license": "MIT", + "dependencies": { + "@fontsource-variable/roboto-flex": "5.2.5", + "@fontsource/material-icons-outlined": "5.2.5", + "@gouvfr-lasuite/cunningham-tokens": "*", + "@internationalized/date": "3.8.0", + "@react-aria/calendar": "3.8.0", + "@react-aria/datepicker": "3.14.2", + "@react-aria/i18n": "3.12.8", + "@react-stately/calendar": "3.8.0", + "@react-stately/datepicker": "3.14.0", + "@tanstack/react-table": "8.21.3", + "@types/react-modal": "3.16.3", + "chromatic": "11.28.2", + "classnames": "2.5.1", + "downshift": "9.0.9", + "react-aria": "3.39.0", + "react-aria-components": "1.8.0", + "react-modal": "3.16.3", + "react-stately": "3.37.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^19.1.2", + "react-dom": "^19.1.2" + } + }, + "node_modules/@gouvfr-lasuite/cunningham-tokens": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@gouvfr-lasuite/cunningham-tokens/-/cunningham-tokens-3.1.0.tgz", + "integrity": "sha512-7HP7E1vjxv3FbFn21XuEqbjQPHP5xucWU0D8+Oj02nIeKLL+0eArTL2GB+c2rPFBt0tAzaLI6A3WlsimOjHN4A==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "commander": "13.1.0", + "deepmerge": "4.3.1", + "figlet": "1.8.1", + "ts-node": "10.9.2" + }, + "bin": { + "cunningham": "dist/bin/Main.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@gouvfr-lasuite/integration": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gouvfr-lasuite/integration/-/integration-1.0.2.tgz", + "integrity": "sha512-npOotZQSyu6SffHiPP+jQVOkJ3qW2KE2cANhEK92sNLX9uZqQaCqljO5GhzsBmh0lB76fiXnrr9i8SIpnDUSZg==", + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "*", + "react-dom": "*", + "typescript": "*" + } + }, + "node_modules/@gouvfr-lasuite/ui-kit": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@gouvfr-lasuite/ui-kit/-/ui-kit-0.19.10.tgz", + "integrity": "sha512-Fn3dLks9O2Lr3iFJVlwptZDmluOBIzAGAUiqA2Wp2/7dQLZu0RaCqdNYc/prvQP9PsuNeHbG0TmrNrjQtQNnbQ==", + "dependencies": { + "@dnd-kit/core": "6.3.1", + "@dnd-kit/modifiers": "9.0.0", + "@dnd-kit/sortable": "10.0.0", + "@fontsource/material-icons": "5.2.5", + "@gouvfr-lasuite/cunningham-react": "4.2.0", + "@gouvfr-lasuite/cunningham-tokens": "3.1.0", + "@gouvfr-lasuite/integration": "1.0.2", + "@types/node": "22.10.7", + "clsx": "2.1.1", + "cmdk": "1.0.4", + "react-arborist": "3.4.3", + "react-aria-components": "1.8.0", + "react-resizable-panels": "2.1.7", + "react-stately": "3.37.0" + }, + "peerDependencies": { + "react": "^19.1.2", + "react-dom": "^19.1.2" + } + }, + "node_modules/@gouvfr-lasuite/ui-kit/node_modules/@types/node": { + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@gouvfr-lasuite/ui-kit/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@internationalized/date": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.0.tgz", + "integrity": "sha512-J51AJ0fEL68hE4CwGPa6E0PO6JDaVLd8aln48xFCSy7CZkZc96dGEGmLs2OEEbBxcsVZtfrqkXJwI2/MSG8yKw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/message": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.8.tgz", + "integrity": "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "intl-messageformat": "^10.1.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", + "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.7.tgz", + "integrity": "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", + "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-aria/autocomplete": { + "version": "3.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@react-aria/autocomplete/-/autocomplete-3.0.0-beta.2.tgz", + "integrity": "sha512-oxsFCIGj5yooQkZzdqjvsdfr9fOlmAq4v6njIOAyQFsta3H0yQiv+YU3XnrnCBxVX+Mz/mZtZgfhAA9JBDukHg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/combobox": "^3.12.2", + "@react-aria/focus": "^3.20.2", + "@react-aria/i18n": "^3.12.8", + "@react-aria/interactions": "^3.25.0", + "@react-aria/listbox": "^3.14.3", + "@react-aria/searchfield": "^3.8.3", + "@react-aria/textfield": "^3.17.2", + "@react-aria/utils": "^3.28.2", + "@react-stately/autocomplete": "3.0.0-beta.1", + "@react-stately/combobox": "^3.10.4", + "@react-types/autocomplete": "3.0.0-alpha.30", + "@react-types/button": "^3.12.0", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/breadcrumbs": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.32.tgz", + "integrity": "sha512-S61vh5DJ2PXiXUwD7gk+pvS/b4VPrc3ZJOUZ0yVRLHkVESr5LhIZH+SAVgZkm1lzKyMRG+BH+fiRH/DZRSs7SA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/link": "^3.8.9", + "@react-aria/utils": "^3.33.1", + "@react-types/breadcrumbs": "^3.7.19", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/breadcrumbs/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/breadcrumbs/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/button": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.14.5.tgz", + "integrity": "sha512-ZuLx+wQj9VQhH9BYe7t0JowmKnns2XrFHFNvIVBb5RwxL+CIycIOL7brhWKg2rGdxvlOom7jhVbcjSmtAaSyaQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/toolbar": "3.0.0-beta.24", + "@react-aria/utils": "^3.33.1", + "@react-stately/toggle": "^3.9.5", + "@react-types/button": "^3.15.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/calendar": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.8.0.tgz", + "integrity": "sha512-9vms/fWjJPZkJcMxciwWWOjGy/Q0nqI6FV0pYbMZbqepkzglEaVd98kl506r/4hLhWKwLdTfqCgbntRecj8jBg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.8.0", + "@react-aria/i18n": "^3.12.8", + "@react-aria/interactions": "^3.25.0", + "@react-aria/live-announcer": "^3.4.2", + "@react-aria/utils": "^3.28.2", + "@react-stately/calendar": "^3.8.0", + "@react-types/button": "^3.12.0", + "@react-types/calendar": "^3.7.0", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/checkbox": { + "version": "3.16.5", + "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.16.5.tgz", + "integrity": "sha512-ZhUT7ELuD52hb+Zpzw0ElLQiVOd5sKYahrh+PK3vq13Wk5TedBscALpjuXetI4pwFfdmAM1Lhgcsrd8+6AmyvA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/form": "^3.1.5", + "@react-aria/interactions": "^3.27.1", + "@react-aria/label": "^3.7.25", + "@react-aria/toggle": "^3.12.5", + "@react-aria/utils": "^3.33.1", + "@react-stately/checkbox": "^3.7.5", + "@react-stately/form": "^3.2.4", + "@react-stately/toggle": "^3.9.5", + "@react-types/checkbox": "^3.10.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/collections": { + "version": "3.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@react-aria/collections/-/collections-3.0.0-rc.0.tgz", + "integrity": "sha512-WcRcE3wKtbprOJlBaMbdYS5Suu2KIGq1gVT2fLXVbmDY0CjGemqp2m5aDblQOO8pxvsAqHV8pyznkhANTnK1CQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.0", + "@react-aria/ssr": "^3.9.8", + "@react-aria/utils": "^3.28.2", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/color": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.1.5.tgz", + "integrity": "sha512-eysWdBRzE8WDhBzh1nfjyUgzseMokXGHjIoJo880T7IPJ8tTavfQni49pU1B2qWrNOWPyrwx4Bd9pzHyboxJSA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/numberfield": "^3.12.5", + "@react-aria/slider": "^3.8.5", + "@react-aria/spinbutton": "^3.7.2", + "@react-aria/textfield": "^3.18.5", + "@react-aria/utils": "^3.33.1", + "@react-aria/visually-hidden": "^3.8.31", + "@react-stately/color": "^3.9.5", + "@react-stately/form": "^3.2.4", + "@react-types/color": "^3.1.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/color/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/color/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/combobox": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.15.0.tgz", + "integrity": "sha512-qSjQTFwKl3x1jCP2NRSJ6doZqAp6c2GTfoiFwWjaWg1IewwLsglaW6NnzqRDFiqFbDGgXPn4MqtC1VYEJ3NEjA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/listbox": "^3.15.3", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/menu": "^3.21.0", + "@react-aria/overlays": "^3.31.2", + "@react-aria/selection": "^3.27.2", + "@react-aria/textfield": "^3.18.5", + "@react-aria/utils": "^3.33.1", + "@react-stately/collections": "^3.12.10", + "@react-stately/combobox": "^3.13.0", + "@react-stately/form": "^3.2.4", + "@react-types/button": "^3.15.1", + "@react-types/combobox": "^3.14.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/combobox/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/combobox/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/datepicker": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.14.2.tgz", + "integrity": "sha512-O7fdzcqIJ7i/+8SGYvx4tloTZgK4Ws8OChdbFcd2rZoRPqxM50M6J+Ota8hTet2wIhojUXnM3x2na3EvoucBXA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.8.0", + "@internationalized/number": "^3.6.1", + "@internationalized/string": "^3.2.6", + "@react-aria/focus": "^3.20.2", + "@react-aria/form": "^3.0.15", + "@react-aria/i18n": "^3.12.8", + "@react-aria/interactions": "^3.25.0", + "@react-aria/label": "^3.7.17", + "@react-aria/spinbutton": "^3.6.14", + "@react-aria/utils": "^3.28.2", + "@react-stately/datepicker": "^3.14.0", + "@react-stately/form": "^3.1.3", + "@react-types/button": "^3.12.0", + "@react-types/calendar": "^3.7.0", + "@react-types/datepicker": "^3.12.0", + "@react-types/dialog": "^3.5.17", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/dialog": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.34.tgz", + "integrity": "sha512-/x53Q5ynpW5Kv9637WYu7SrDfj3woSp6jJRj8l6teGnWW/iNZWYJETgzHfbxx+HPKYATCZesRoIeO2LnYIXyEA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/overlays": "^3.31.2", + "@react-aria/utils": "^3.33.1", + "@react-types/dialog": "^3.5.24", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/disclosure": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@react-aria/disclosure/-/disclosure-3.1.3.tgz", + "integrity": "sha512-S3k7Wqrj+x0sWcP88Z1stSr5TIZmKEmx2rU7RB1O1/jPpbw5mgKnjtiriOlTh+kwdK11FkeqgxyHzAcBAR+FMQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-stately/disclosure": "^3.0.11", + "@react-types/button": "^3.15.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/dnd": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@react-aria/dnd/-/dnd-3.11.6.tgz", + "integrity": "sha512-4YLHUeYJleF+moAYaYt8UZqujudPvpoaHR+QMkWIFzhfridVUhCr6ZjGWrzpSZY3r68k46TG7YCsi4IEiNnysw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/string": "^3.2.7", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/overlays": "^3.31.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/collections": "^3.12.10", + "@react-stately/dnd": "^3.7.4", + "@react-types/button": "^3.15.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/dnd/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/dnd/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.21.5", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.5.tgz", + "integrity": "sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/form": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.1.5.tgz", + "integrity": "sha512-BWlONgHn8hmaMkcS6AgMSLQeNqVBwqPNLhdqjDO/PCfzvV7O8NZw/dFeIzJwfG4aBfSpbHHRdXGdfrk3d8dylQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-stately/form": "^3.2.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid": { + "version": "3.14.8", + "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.14.8.tgz", + "integrity": "sha512-X6rRFKDu/Kh6Sv8FBap3vjcb+z4jXkSOwkYnexIJp5kMTo5/Dqo55cCBio5B70Tanfv32Ev/6SpzYG7ryxnM9w==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/collections": "^3.12.10", + "@react-stately/grid": "^3.11.9", + "@react-stately/selection": "^3.20.9", + "@react-types/checkbox": "^3.10.4", + "@react-types/grid": "^3.3.8", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/grid/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/grid/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/gridlist": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/@react-aria/gridlist/-/gridlist-3.14.4.tgz", + "integrity": "sha512-C/SbwC0qagZatoBrCjx8iZUex9apaJ8o8iRJ9eVHz0cpj7mXg6HuuotYGmDy9q67A2hve4I693RM1Cuwqwm+PQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/grid": "^3.14.8", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/list": "^3.13.4", + "@react-stately/tree": "^3.9.6", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/gridlist/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/gridlist/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/i18n": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.8.tgz", + "integrity": "sha512-V/Nau9WuwTwxfFffQL4URyKyY2HhRlu9zmzkF2Hw/j5KmEQemD+9jfaLueG2CJu85lYL06JrZXUdnhZgKnqMkA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.8.0", + "@internationalized/message": "^3.1.7", + "@internationalized/number": "^3.6.1", + "@internationalized/string": "^3.2.6", + "@react-aria/ssr": "^3.9.8", + "@react-aria/utils": "^3.28.2", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.1.tgz", + "integrity": "sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/label": { + "version": "3.7.25", + "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.25.tgz", + "integrity": "sha512-oNK3Pqj4LDPwEbQaoM/uCip4QvQmmwGOh08VeW+vzSi6TAwf+KoWTyH/tiAeB0CHWNDK0k3e1iTygTAt4wzBmg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/landmark": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.10.tgz", + "integrity": "sha512-GpNjJaI8/a6WxYDZgzTCLYSzPM6xp2pxCIQ4udiGbTCtxx13Trmm0cPABvPtzELidgolCf05em9Phr+3G0eE8A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/link": { + "version": "3.8.9", + "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.8.9.tgz", + "integrity": "sha512-UaAFBfs84/Qq6TxlMWkREqqNY6SFLukot+z2Aa1kC+VyStv1kWG6sE5QLjm4SBn1Q3CGRsefhB/5+taaIbB4Pw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-types/link": "^3.6.7", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/listbox": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.15.3.tgz", + "integrity": "sha512-C6YgiyrHS5sbS5UBdxGMhEs+EKJYotJgGVtl9l0ySXpBUXERiHJWLOyV7a8PwkUOmepbB4FaLD7Y9EUzGkrGlw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/label": "^3.7.25", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/collections": "^3.12.10", + "@react-stately/list": "^3.13.4", + "@react-types/listbox": "^3.7.6", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/live-announcer": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.4.4.tgz", + "integrity": "sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/menu": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.21.0.tgz", + "integrity": "sha512-CKTVZ4izSE1eKIti6TbTtzJAUo+WT8O4JC0XZCYDBpa0f++lD19Kz9aY+iY1buv5xGI20gAfpO474E9oEd4aQA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/overlays": "^3.31.2", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/collections": "^3.12.10", + "@react-stately/menu": "^3.9.11", + "@react-stately/selection": "^3.20.9", + "@react-stately/tree": "^3.9.6", + "@react-types/button": "^3.15.1", + "@react-types/menu": "^3.10.7", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/menu/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/menu/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/meter": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@react-aria/meter/-/meter-3.4.30.tgz", + "integrity": "sha512-ZmANKW7s/Z4QGylHi46nhwtQ47T1bfMsU9MysBu7ViXXNJ03F4b6JXCJlKL5o2goQ3NbfZ68GeWamIT0BWSgtw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/progress": "^3.4.30", + "@react-types/meter": "^3.4.15", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/numberfield": { + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.12.5.tgz", + "integrity": "sha512-Fi41IUWXEHLFIeJ/LHuZ9Azs8J/P563fZi37GSBkIq5P1pNt1rPgJJng5CNn4KsHxwqadTRUlbbZwbZraWDtRg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/spinbutton": "^3.7.2", + "@react-aria/textfield": "^3.18.5", + "@react-aria/utils": "^3.33.1", + "@react-stately/form": "^3.2.4", + "@react-stately/numberfield": "^3.11.0", + "@react-types/button": "^3.15.1", + "@react-types/numberfield": "^3.8.18", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/numberfield/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/numberfield/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/overlays": { + "version": "3.31.2", + "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.31.2.tgz", + "integrity": "sha512-78HYI08r6LvcfD34gyv19ArRIjy1qxOKuXl/jYnjLDyQzD4pVb634IQWcm0zt10RdKgyuH6HTqvuDOgZTLet7Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-aria/visually-hidden": "^3.8.31", + "@react-stately/flags": "^3.1.2", + "@react-stately/overlays": "^3.6.23", + "@react-types/button": "^3.15.1", + "@react-types/overlays": "^3.9.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/overlays/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/overlays/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/progress": { + "version": "3.4.30", + "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.30.tgz", + "integrity": "sha512-S6OWVGgluSWYSd/A6O8CVjz83eeMUfkuWSra0ewAV9bmxZ7TP9pUmD3bGdqHZEl97nt5vHGjZ3eq/x8eCmzKhA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/label": "^3.7.25", + "@react-aria/utils": "^3.33.1", + "@react-types/progress": "^3.5.18", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/progress/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/progress/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/radio": { + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.12.5.tgz", + "integrity": "sha512-8CCJKJzfozEiWBPO9QAATG1rBGJEJ+xoqvHf9LKU2sPFGsA2/SRnLs6LB9fCG5R3spvaK1xz0any1fjWPl7x8A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/form": "^3.1.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/label": "^3.7.25", + "@react-aria/utils": "^3.33.1", + "@react-stately/radio": "^3.11.5", + "@react-types/radio": "^3.9.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/radio/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/radio/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/searchfield": { + "version": "3.8.12", + "resolved": "https://registry.npmjs.org/@react-aria/searchfield/-/searchfield-3.8.12.tgz", + "integrity": "sha512-kYlUHD/+mWzNroHoR8ojUxYBoMviRZn134WaKPFjfNUGZDOEuh4XzOoj+cjdJfe6N3mwTaYu6rJQtunSHIAfhA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/textfield": "^3.18.5", + "@react-aria/utils": "^3.33.1", + "@react-stately/searchfield": "^3.5.19", + "@react-types/button": "^3.15.1", + "@react-types/searchfield": "^3.6.8", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/searchfield/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/searchfield/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/select": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/@react-aria/select/-/select-3.17.3.tgz", + "integrity": "sha512-u0UFWw0S7q9oiSbjetDpRoLLIcC+L89uYlm+YfCrdT8ntbQgABNiJRxdVvxnhR0fR6MC9ASTTvuQnNHNn52+1A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/form": "^3.1.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/label": "^3.7.25", + "@react-aria/listbox": "^3.15.3", + "@react-aria/menu": "^3.21.0", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-aria/visually-hidden": "^3.8.31", + "@react-stately/select": "^3.9.2", + "@react-types/button": "^3.15.1", + "@react-types/select": "^3.12.2", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/select/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/select/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection": { + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.27.2.tgz", + "integrity": "sha512-GbUSSLX/ciXix95KW1g+SLM9np7iXpIZrFDSXkC6oNx1uhy18eAcuTkeZE25+SY5USVUmEzjI3m/3JoSUcebbg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-stately/selection": "^3.20.9", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/selection/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/selection/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/separator": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/@react-aria/separator/-/separator-3.4.16.tgz", + "integrity": "sha512-RCUtQhDGnPxKzyG8KM79yOB0fSiEf8r/rxShidOVnGLiBW2KFmBa22/Gfc4jnqg/keN3dxvkSGoqmeXgctyp6g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/slider": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.8.5.tgz", + "integrity": "sha512-gqkJxznk141mE0JamXF5CXml9PDbPkBz8dyKlihtWHWX4yhEbVYdC9J0otE7iCR3zx69Bm7WHoTGL0BsdpKzVA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/label": "^3.7.25", + "@react-aria/utils": "^3.33.1", + "@react-stately/slider": "^3.7.5", + "@react-types/shared": "^3.33.1", + "@react-types/slider": "^3.8.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/slider/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/slider/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.7.2.tgz", + "integrity": "sha512-adjE1wNCWlugvAtVXlXWPtIG9JWurEgYVn1Eeyh19x038+oXGvOsOAoKCXM+SnGleTWQ9J7pEZITFoEI3cVfAw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/utils": "^3.33.1", + "@react-types/button": "^3.15.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/spinbutton/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/spinbutton/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/switch": { + "version": "3.7.11", + "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.7.11.tgz", + "integrity": "sha512-dYVX71HiepBsKyeMaQgHbhqI+MQ3MVoTd5EnTbUjefIBnmQZavYj1/e4NUiUI4Ix+/C0HxL8ibDAv4NlSW3eLQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/toggle": "^3.12.5", + "@react-stately/toggle": "^3.9.5", + "@react-types/shared": "^3.33.1", + "@react-types/switch": "^3.5.17", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/table": { + "version": "3.17.11", + "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.17.11.tgz", + "integrity": "sha512-GkYmWPiW3OM+FUZxdS33teHXHXde7TjHuYgDDaG9phvg6cQTQjGilJozrzA3OfftTOq5VB8XcKTIQW3c0tpYsQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/grid": "^3.14.8", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/live-announcer": "^3.4.4", + "@react-aria/utils": "^3.33.1", + "@react-aria/visually-hidden": "^3.8.31", + "@react-stately/collections": "^3.12.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/table": "^3.15.4", + "@react-types/checkbox": "^3.10.4", + "@react-types/grid": "^3.3.8", + "@react-types/shared": "^3.33.1", + "@react-types/table": "^3.13.6", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/table/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/table/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tabs": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.11.1.tgz", + "integrity": "sha512-3Ppz7yaEDW9L7p9PE9yNOl5caLwNnnLQqI+MX/dwbWlw9HluHS7uIjb21oswNl6UbSxAWyENOka45+KN4Fkh7A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/tabs": "^3.8.9", + "@react-types/shared": "^3.33.1", + "@react-types/tabs": "^3.3.22", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tabs/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/tabs/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tag": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-aria/tag/-/tag-3.8.1.tgz", + "integrity": "sha512-VonpO++F8afXGDWc9VUxAc2wefyJpp1n9OGpbnB7zmqWiuPwO/RixjUdcH7iJkiC4vADwx9uLnhyD6kcwGV2ig==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/gridlist": "^3.14.4", + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/label": "^3.7.25", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/list": "^3.13.4", + "@react-types/button": "^3.15.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tag/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/tag/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/textfield": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.18.5.tgz", + "integrity": "sha512-ttwVSuwoV3RPaG2k2QzEXKeQNQ3mbdl/2yy6I4Tjrn1ZNkYHfVyJJ26AjenfSmj1kkTQoSAfZ8p+7rZp4n0xoQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/form": "^3.1.5", + "@react-aria/interactions": "^3.27.1", + "@react-aria/label": "^3.7.25", + "@react-aria/utils": "^3.33.1", + "@react-stately/form": "^3.2.4", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@react-types/textfield": "^3.12.8", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toast": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.11.tgz", + "integrity": "sha512-2DjZjBAvm8/CWbnZ6s7LjkYCkULKtjMve6GvhPTq98AthuEDLEiBvM1wa3xdecCRhZyRT1g6DXqVca0EfZ9fJA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/landmark": "^3.0.10", + "@react-aria/utils": "^3.33.1", + "@react-stately/toast": "^3.1.3", + "@react-types/button": "^3.15.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toast/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/toast/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toggle": { + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.12.5.tgz", + "integrity": "sha512-XXVFLzcV8fr9mz7y/wfxEAhWvaBZ9jSfhCMuxH2bsivO7nTcMJ1jb4g2xJNwZgne17bMWNc7mKvW5dbsdlI6BA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-stately/toggle": "^3.9.5", + "@react-types/checkbox": "^3.10.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toolbar": { + "version": "3.0.0-beta.24", + "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.24.tgz", + "integrity": "sha512-B2Rmpko7Ghi2RbNfsGdbR7I+RQBDhPGVE4bU3/EwHz+P/vNe5LyGPTeSwqaOMsQTF9lKNCkY8424dVTCr6RUMg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.21.5", + "@react-aria/i18n": "^3.12.16", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/toolbar/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/toolbar/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tooltip": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.9.2.tgz", + "integrity": "sha512-VrgkPwHiEnAnBhoQ4W7kfry/RfVuRWrUPaJSp0+wKM6u0gg2tmn7OFRDXTxBAm/omQUguIdIjRWg7sf3zHH82A==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-stately/tooltip": "^3.5.11", + "@react-types/shared": "^3.33.1", + "@react-types/tooltip": "^3.5.2", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tree": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@react-aria/tree/-/tree-3.1.7.tgz", + "integrity": "sha512-C54yH5NmsOFa2Q+cg6B1BPr5KUlU9vLIoBnVrgrH237FRSXQPIbcM4VpmITAHq1VR7w6ayyS1hgTwFxo67ykWQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/gridlist": "^3.14.4", + "@react-aria/i18n": "^3.12.16", + "@react-aria/selection": "^3.27.2", + "@react-aria/utils": "^3.33.1", + "@react-stately/tree": "^3.9.6", + "@react-types/button": "^3.15.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/tree/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/tree/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.1.tgz", + "integrity": "sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/virtualizer": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@react-aria/virtualizer/-/virtualizer-4.1.13.tgz", + "integrity": "sha512-d5KS+p8GXGNRbGPRE/N6jtth3et3KssQIz52h2+CAoAh7C3vvR64kkTaGdeywClvM+fSo8FxJuBrdfQvqC2ktQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/i18n": "^3.12.16", + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-stately/virtualizer": "^4.4.6", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/virtualizer/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/virtualizer/node_modules/@react-aria/i18n": { + "version": "3.12.16", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.16.tgz", + "integrity": "sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@internationalized/message": "^3.1.8", + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/visually-hidden": { + "version": "3.8.31", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.31.tgz", + "integrity": "sha512-RTOHHa4n56a9A3criThqFHBifvZoV71+MCkSuNP2cKO662SUWjqKkd0tJt/mBRMEJPkys8K7Eirp6T8Wt5FFRA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-dnd/asap": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==", + "license": "MIT" + }, + "node_modules/@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==", + "license": "MIT" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", + "license": "MIT" + }, + "node_modules/@react-stately/autocomplete": { + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@react-stately/autocomplete/-/autocomplete-3.0.0-beta.1.tgz", + "integrity": "sha512-ohs6QOtJouQ+Y1+zRKiCzv57QogSTRuOA1QfrnIS1YPwKO1EDQXSqFkq2htK5+bN9GCm94yo6r4iX++SZKmLXA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.10.6", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/calendar": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.8.0.tgz", + "integrity": "sha512-YAuJiR9EtVThX91gU2ay/6YgPe0LvZWEssu4BS0Atnwk5cAo32gvF5FMta9ztH1LIULdZFaypU/C1mvnayMf+Q==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.8.0", + "@react-stately/utils": "^3.10.6", + "@react-types/calendar": "^3.7.0", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/checkbox": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.7.5.tgz", + "integrity": "sha512-K5R5ted7AxLB3sDkuVAazUdyRMraFT1imVqij2GuAiOUFvsZvbuocnDuFkBVKojyV3GpqLBvViV8IaCMc4hNIw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.2.4", + "@react-stately/utils": "^3.11.0", + "@react-types/checkbox": "^3.10.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/collections": { + "version": "3.12.10", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.10.tgz", + "integrity": "sha512-wmF9VxJDyBujBuQ76vXj2g/+bnnj8fx5DdXgRmyfkkYhPB46+g2qnjbVGEvipo7bJuGxDftCUC4SN7l7xqUWfg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/color": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.9.5.tgz", + "integrity": "sha512-8pZxzXWDRuglzDwyTG7mLw2LQMCHIVNbVc9YmbsxbOjAL+lOqszo60KzyaFKVxeDQczSvrNTHcQZqlbNIC0eyQ==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/number": "^3.6.5", + "@internationalized/string": "^3.2.7", + "@react-stately/form": "^3.2.4", + "@react-stately/numberfield": "^3.11.0", + "@react-stately/slider": "^3.7.5", + "@react-stately/utils": "^3.11.0", + "@react-types/color": "^3.1.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/combobox": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.13.0.tgz", + "integrity": "sha512-dX9g/cK1hjLRjcbWVF6keHxTQDGhKGB2QAgPhWcBmOK3qJv+2dQqsJ6YCGWn/Y2N2acoEseLrAA7+Qe4HWV9cg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.10", + "@react-stately/form": "^3.2.4", + "@react-stately/list": "^3.13.4", + "@react-stately/overlays": "^3.6.23", + "@react-stately/utils": "^3.11.0", + "@react-types/combobox": "^3.14.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/data": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/@react-stately/data/-/data-3.15.2.tgz", + "integrity": "sha512-BsmeeGgFwOGwo0g9Waprdyt+846n3KhKggZfpEnp5+sC4dE4uW1VIYpdyupMfr3bQcmX123q6TegfNP3eszrUA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/datepicker": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.14.0.tgz", + "integrity": "sha512-JSkQfKW0+WpPQyOOeRPBLwXkVfpTUwgZJDnHBCud5kEuQiFFyeAIbL57RNXc4AX2pzY3piQa6OHnjDGTfqClxQ==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.8.0", + "@internationalized/string": "^3.2.6", + "@react-stately/form": "^3.1.3", + "@react-stately/overlays": "^3.6.15", + "@react-stately/utils": "^3.10.6", + "@react-types/datepicker": "^3.12.0", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/disclosure": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@react-stately/disclosure/-/disclosure-3.0.11.tgz", + "integrity": "sha512-/KjB/0HkxGWbhFAPztCP411LUKZCx9k8cKukrlGqrUWyvrcXlmza90j0g/CuxACBoV+DJP9V+4q+8ide0x750A==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/dnd": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-stately/dnd/-/dnd-3.7.4.tgz", + "integrity": "sha512-YD0TVR5JkvTqskc1ouBpVKs6t/QS4RYCIyu8Ug8RgO122iIizuf2pfKnRLjYMdu5lXzBXGaIgd49dvnLzEXHIw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/selection": "^3.20.9", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/form": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.2.4.tgz", + "integrity": "sha512-qNBzun8SbLdgahryhKLqL1eqP+MXY6as82sVXYOOvUYLzgU5uuN8mObxYlxJgMI5akSdQJQV3RzyfVobPRE7Kw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/grid": { + "version": "3.11.9", + "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.11.9.tgz", + "integrity": "sha512-qQY6F+27iZRn30dt0ZOrSetUmbmNJ0pLe9Weuqw3+XDVSuWT+2O/rO1UUYeK+mO0Acjzdv+IWiYbu9RKf2wS9w==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.10", + "@react-stately/selection": "^3.20.9", + "@react-types/grid": "^3.3.8", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/layout": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@react-stately/layout/-/layout-4.6.0.tgz", + "integrity": "sha512-kBenEsP03nh5rKgfqlVMPcoKTJv0v92CTvrAb5gYY8t9g8LOwzdL89Yannq7f5xv8LFck/MmRQlotpMt2InETg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.10", + "@react-stately/table": "^3.15.4", + "@react-stately/virtualizer": "^4.4.6", + "@react-types/grid": "^3.3.8", + "@react-types/shared": "^3.33.1", + "@react-types/table": "^3.13.6", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/list": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.13.4.tgz", + "integrity": "sha512-HHYSjA9VG7FPSAtpXAjQyM/V7qFHWGg88WmMrDt5QDlTBexwPuH0oFLnW0qaVZpAIxuWIsutZfxRAnme/NhhAA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.10", + "@react-stately/selection": "^3.20.9", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/menu": { + "version": "3.9.11", + "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.9.11.tgz", + "integrity": "sha512-vYkpO9uV2OUecsIkrOc+Urdl/s1xw/ibNH/UXsp4PtjMnS6mK9q2kXZTM3WvMAKoh12iveUO+YkYCZQshmFLHQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/overlays": "^3.6.23", + "@react-types/menu": "^3.10.7", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/numberfield": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.11.0.tgz", + "integrity": "sha512-rxfC047vL0LP4tanjinfjKAriAvdVL57Um5RUL5nHML8IOWCB3TBxegQkJ6to6goScC/oZhd0/Y2LSaiRuKbNw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/number": "^3.6.5", + "@react-stately/form": "^3.2.4", + "@react-stately/utils": "^3.11.0", + "@react-types/numberfield": "^3.8.18", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/overlays": { + "version": "3.6.23", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.23.tgz", + "integrity": "sha512-RzWxots9A6gAzQMP4s8hOAHV7SbJRTFSlQbb6ly1nkWQXacOSZSFNGsKOaS0eIatfNPlNnW4NIkgtGws5UYzfw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/overlays": "^3.9.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/radio": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.11.5.tgz", + "integrity": "sha512-QxA779S4ea5icQ0ja7CeiNzY1cj7c9G9TN0m7maAIGiTSinZl2Ia8naZJ0XcbRRp+LBll7RFEdekne15TjvS/w==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.2.4", + "@react-stately/utils": "^3.11.0", + "@react-types/radio": "^3.9.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/searchfield": { + "version": "3.5.19", + "resolved": "https://registry.npmjs.org/@react-stately/searchfield/-/searchfield-3.5.19.tgz", + "integrity": "sha512-URllgjbtTQEaOCfddbHpJSPKOzG3pE3ajQHJ7Df8qCoHTjKfL6hnm/vp7X5sxPaZaN7VLZ5kAQxTE8hpo6s0+A==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/searchfield": "^3.6.8", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/select": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.9.2.tgz", + "integrity": "sha512-oWn0bijuusp8YI7FRM/wgtPVqiIrgU/ZUfLKe/qJUmT8D+JFaMAJnyrAzKpx98TrgamgtXynF78ccpopPhgrKQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/form": "^3.2.4", + "@react-stately/list": "^3.13.4", + "@react-stately/overlays": "^3.6.23", + "@react-stately/utils": "^3.11.0", + "@react-types/select": "^3.12.2", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/selection": { + "version": "3.20.9", + "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.20.9.tgz", + "integrity": "sha512-RhxRR5Wovg9EVi3pq7gBPK2BoKmP59tOXDMh2r1PbnGevg/7TNdR67DCEblcmXwHuBNS46ELfKdd0XGHqmS8nQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.10", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/slider": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.7.5.tgz", + "integrity": "sha512-OrQMNR5xamLYH52TXtvTgyw3EMwv+JI+1istQgEj1CHBjC9eZZqn5iNCN20tzm+uDPTH0EIGULFjjPIumqYUQg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@react-types/slider": "^3.8.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/table": { + "version": "3.15.4", + "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.15.4.tgz", + "integrity": "sha512-fGaNyw3wv7JgRCNzgyDzpaaTFuSy5f4Qekch4UheMXDJX7dOeaMhUXeOfvnXCVg+BGM4ey/D82RvDOGvPy1Nww==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/grid": "^3.11.9", + "@react-stately/selection": "^3.20.9", + "@react-stately/utils": "^3.11.0", + "@react-types/grid": "^3.3.8", + "@react-types/shared": "^3.33.1", + "@react-types/table": "^3.13.6", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tabs": { + "version": "3.8.9", + "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.8.9.tgz", + "integrity": "sha512-AQ4Xrn6YzIolaVShCV9cnwOjBKPAOGP/PTp7wpSEtQbQ0HZzUDG2RG/M4baMeUB2jZ33b7ifXyPcK78o0uOftg==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/list": "^3.13.4", + "@react-types/shared": "^3.33.1", + "@react-types/tabs": "^3.3.22", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/toast": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.1.3.tgz", + "integrity": "sha512-mT9QJKmD523lqFpOp0VWZ6QHZENFK7HrodnNJDVc7g616s5GNmemdlkITV43fSY3tHeThCVvPu+Uzh7RvQ9mpQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/toggle": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.9.5.tgz", + "integrity": "sha512-PVzXc788q3jH98Kvw1LYDL+wpVC14dCEKjOku8cSaqhEof6AJGaLR9yq+EF1yYSL2dxI6z8ghc0OozY8WrcFcA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/utils": "^3.11.0", + "@react-types/checkbox": "^3.10.4", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tooltip": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.5.11.tgz", + "integrity": "sha512-o8PnFXbvDCuVZ4Ht9ahfS6KHwIZjXopvoQ2vUPxv920irdgWEeC+4omgDOnJ/xFvcpmmJAmSsrQsTQrTguDUQA==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/overlays": "^3.6.23", + "@react-types/tooltip": "^3.5.2", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/tree": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.9.6.tgz", + "integrity": "sha512-JCuhGyX2A+PAMsx2pRSwArfqNFZJ9JSPkDaOQJS8MFPAsBe5HemvXsdmv9aBIMzlbCYcVq6EsrFnzbVVTBt/6w==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/collections": "^3.12.10", + "@react-stately/selection": "^3.20.9", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/virtualizer": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-4.4.6.tgz", + "integrity": "sha512-9SfXgLFB61/8SXNLfg5ARx9jAK4m03Aw6/Cg8mdZN24SYarL4TKNRpfw8K/HHVU/bi6WHSJypk6Z/z19o/ztrg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/autocomplete": { + "version": "3.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@react-types/autocomplete/-/autocomplete-3.0.0-alpha.30.tgz", + "integrity": "sha512-9neGygI+stJqiEFHzoc1jMySj6lOc4MUmBmu0uGn2zdOG2zxaAZSjh1pd9AJkHNyZ4j/n5rVXMo+v3RNkUntNw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/combobox": "^3.13.4", + "@react-types/searchfield": "^3.6.1", + "@react-types/shared": "^3.29.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/breadcrumbs": { + "version": "3.7.19", + "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.19.tgz", + "integrity": "sha512-AnkyYYmzaM2QFi/N0P/kQLM8tHOyFi7p397B/jEMucXDfwMw5Ny1ObCXeIEqbh8KrIa2Xp8SxmQlCV+8FPs4LA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/link": "^3.6.7", + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/button": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.15.1.tgz", + "integrity": "sha512-M1HtsKreJkigCnqceuIT22hDJBSStbPimnpmQmsl7SNyqCFY3+DHS7y/Sl3GvqCkzxF7j9UTL0dG38lGQ3K4xQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/calendar": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.8.3.tgz", + "integrity": "sha512-fpH6WNXotzH0TlKHXXxtjeLZ7ko0sbyHmwDAwmDFyP7T0Iwn1YQZ+lhceLifvynlxuOgX6oBItyUKmkHQ0FouQ==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/calendar/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-types/checkbox": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.10.4.tgz", + "integrity": "sha512-tYCG0Pd1usEz5hjvBEYcqcA0youx930Rss1QBIse9TgMekA1c2WmPDNupYV8phpO8Zuej3DL1WfBeXcgavK8aw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/color": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.1.4.tgz", + "integrity": "sha512-s+Xj4pvNBlJPpQ1Gr7bO1j4/tuwMUfdS9xIVFuiW5RvDsSybKTUJ/gqPzTxms94VDCRhLFocVn2STNdD2Erf6A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1", + "@react-types/slider": "^3.8.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/combobox": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.14.0.tgz", + "integrity": "sha512-zmSSS7BcCOD8rGT8eGbVy7UlL5qq1vm88fFn4WgFe+lfK33ne+E7yTzTxcPY2TCGSo5fY6xMj3OG79FfVNGbSg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/datepicker": { + "version": "3.13.5", + "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.13.5.tgz", + "integrity": "sha512-j28Vz+xvbb4bj7+9Xbpc4WTvSitlBvt7YEaEGM/8ZQ5g4Jr85H2KwkmDwjzmMN2r6VMQMMYq9JEcemq5wWpfUQ==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.0", + "@react-types/calendar": "^3.8.3", + "@react-types/overlays": "^3.9.4", + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/datepicker/node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-types/dialog": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.24.tgz", + "integrity": "sha512-NFurEP/zV0dA/41422lV1t+0oh6f/13n+VmLHZG8R13m1J3ql/kAXZ49zBSqkqANBO1ojyugWebk99IiR4pYOw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.9.4", + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/form": { + "version": "3.7.18", + "resolved": "https://registry.npmjs.org/@react-types/form/-/form-3.7.18.tgz", + "integrity": "sha512-0sBJW0+I9nJcF4SmKrYFEWAlehiebSTy7xqriqAXtqfTEdvzAYLGaAK2/7gx+wlNZeDTdW43CDRJ4XAhyhBqnw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/grid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.3.8.tgz", + "integrity": "sha512-zJvXH8gc1e1VH2H3LRnHH/W2HIkLkZMH3Cu5pLcj0vDuLBSWpcr3Ikh3jZ+VUOZF0G1Jt1lO8pKIaqFzDLNmLQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/link": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.6.7.tgz", + "integrity": "sha512-1apXCFJgMC1uydc2KNENrps1qR642FqDpwlNWe254UTpRZn/hEZhA6ImVr8WhomfLJu672WyWA0rUOv4HT+/pQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/listbox": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.7.6.tgz", + "integrity": "sha512-335NYElKEByXMalAmeRPyulKIDd2cjOCQhLwvv2BtxO5zaJfZnBbhZs+XPd9zwU6YomyOxODKSHrwbNDx+Jf3w==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/menu": { + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.10.7.tgz", + "integrity": "sha512-+p7ixZdvPDJZhisqdtWiiuJ9pteNfK5i19NB6wzAw5XkljbEzodNhwLv6rI96DY5XpbFso2kcjw7IWi+rAAGGQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.9.4", + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/meter": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/@react-types/meter/-/meter-3.4.15.tgz", + "integrity": "sha512-9WjNphhLLM+TA4Ev1y2MkpugJ5JjTXseHh7ZWWx2veq5DrXMZYclkRpfUrUdLVKvaBIPQCgpQIj0TcQi+quR9A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/progress": "^3.5.18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/numberfield": { + "version": "3.8.18", + "resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.8.18.tgz", + "integrity": "sha512-nLzk7YAG9yAUtSv+9R8LgCHsu8hJq8/A+m1KsKxvc8WmNJjIujSFgWvT21MWBiUgPBzJKGzAqpMDDa087mltJQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/overlays": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.4.tgz", + "integrity": "sha512-7Z9HaebMFyYBqtv3XVNHEmVkm7AiYviV7gv0c98elEN2Co+eQcKFGvwBM9Gy/lV57zlTqFX1EX/SAqkMEbCLOA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/progress": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.18.tgz", + "integrity": "sha512-mKeQn+KrHr1y0/k7KtrbeDGDaERH6i4f6yBwj/ZtYDCTNKMO3tPHJY6nzF0w/KKZLplIO+BjUbHXc2RVm8ovwQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/radio": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.9.4.tgz", + "integrity": "sha512-TkMRY3sA1PcFZhhclu4IUzUTIir6MzNJj8h6WT8vO6Nug2kXJ72qigugVFBWJSE472mltduOErEAo0rtAYWbQA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/searchfield": { + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/@react-types/searchfield/-/searchfield-3.6.8.tgz", + "integrity": "sha512-M2p7OVdMTMDmlBcHd4N2uCBwg3uJSNM4lmEyf09YD44N5wDAI0yogk52QBwsnhpe+i2s65UwCYgunB+QltRX8A==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1", + "@react-types/textfield": "^3.12.8" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/select": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.12.2.tgz", + "integrity": "sha512-AseOjfr3qM1W1qIWcbAe6NFpwZluVeQX/dmu9BYxjcnVvtoBLPMbE5zX/BPbv+N5eFYjoMyj7Ug9dqnI+LrlGw==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.1.tgz", + "integrity": "sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/slider": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.8.4.tgz", + "integrity": "sha512-C+xFVvfKREai9S/ekBDCVaGPOQYkNUAsQhjQnNsUAATaox4I6IYLmcIgLmljpMQWqAe+gZiWsIwacRYMez2Tew==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/switch": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.17.tgz", + "integrity": "sha512-2GTPJvBCYI8YZ3oerHtXg+qikabIXCMJ6C2wcIJ5Xn0k9XOovowghfJi10OPB2GGyOiLBU74CczP5nx8adG90Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/table": { + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.13.6.tgz", + "integrity": "sha512-eluL+iFfnVmFm7OSZrrFG9AUjw+tcv898zbv+NsZACa8oXG1v9AimhZfd+Mo8q/5+sX/9hguWNXFkSvmTjuVPQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/grid": "^3.3.8", + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/tabs": { + "version": "3.3.22", + "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.22.tgz", + "integrity": "sha512-HGwLD9dA3k3AGfRKGFBhNgxU9/LyRmxN0kxVj1ghA4L9S/qTOzS6GhrGNkGzsGxyVLV4JN8MLxjWN2o9QHnLEg==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/textfield": { + "version": "3.12.8", + "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.12.8.tgz", + "integrity": "sha512-wt6FcuE5AyntxsnPika/h3nf/DPmeAVbI018L9o6h+B/IL4sMWWdx663wx2KOOeHH8ejKGZQNPLhUKs4s1mVQA==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/tooltip": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.5.2.tgz", + "integrity": "sha512-FvSuZ2WP08NEWefrpCdBYpEEZh/5TvqvGjq0wqGzWg2OPwpc14HjD8aE7I3MOuylXkD4MSlMjl7J4DlvlcCs3Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-types/overlays": "^3.9.4", + "@react-types/shared": "^3.33.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.91.4", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.91.4.tgz", + "integrity": "sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.48.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz", + "integrity": "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.91.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz", + "integrity": "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.93.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.20", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@viselect/react": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@viselect/react/-/react-3.9.0.tgz", + "integrity": "sha512-uz3ehdawjXwTUGnDKyNfmecviLAjUIG7cOQdeKcdXiSCHg9ZS12Vhr/j0shuHjX0xPvy9IqlbVuvzOHq77QDQw==", + "license": "MIT", + "dependencies": { + "@viselect/vanilla": "3.9.0" + }, + "peerDependencies": { + "react": ">=19.0.0" + } + }, + "node_modules/@viselect/vanilla": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@viselect/vanilla/-/vanilla-3.9.0.tgz", + "integrity": "sha512-E9eBgoi/crJ0SlZMAc+Yst7nU324LZ5LLvcXjzWEcrfllscdpTml2OLOKHC7O8Bbz19OybSLv6VexxnjlJrLxQ==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/calendars": { + "resolved": "apps/calendars", + "link": true + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chromatic": { + "version": "11.28.2", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.28.2.tgz", + "integrity": "sha512-aCmUPcZUs4/p9zRZdMreOoO/5JqO2DiJC3md1/vRx8dlMRcmR/YI5ZbgXZcai2absVR+6hsXZ5XiPxV2sboTuQ==", + "license": "MIT", + "bin": { + "chroma": "dist/bin.js", + "chromatic": "dist/bin.js", + "chromatic-cli": "dist/bin.js" + }, + "peerDependencies": { + "@chromatic-com/cypress": "^0.*.* || ^1.0.0", + "@chromatic-com/playwright": "^0.*.* || ^1.0.0" + }, + "peerDependenciesMeta": { + "@chromatic-com/cypress": { + "optional": true + }, + "@chromatic-com/playwright": { + "optional": true + } + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", + "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.0", + "use-sync-external-store": "^1.2.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devalue": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dnd-core": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", + "license": "MIT", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, + "node_modules/dnd-core/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/downshift": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-9.0.9.tgz", + "integrity": "sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "compute-scroll-into-view": "^3.1.0", + "prop-types": "^15.8.1", + "react-is": "18.2.0", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/e2e": { + "resolved": "apps/e2e", + "link": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.30.1", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", + "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.1.6", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", + "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", + "license": "BSD-3-Clause" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/figlet": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.1.tgz", + "integrity": "sha512-kEC3Sme+YvA8Hkibv0NR1oClGcWia0VB2fC1SlMy027cwe795Xx40Xiv/nw/iFAwQLupymWh+uhAAErn/7hwPg==", + "license": "MIT", + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/i18next": { + "version": "25.8.14", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.14.tgz", + "integrity": "sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.1.tgz", + "integrity": "sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==", + "license": "MIT" + }, + "node_modules/ical.js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz", + "integrity": "sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==", + "license": "MPL-2.0" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-i18next": { + "version": "15.4.3", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.4.3.tgz", + "integrity": "sha512-ZRmiz72o1Jvh2ZghCUQX1Ua5F/f2W1/Ila/L1ZeKVuSWiH7J4zfUedfDxNBEhj9lajREC7aoJuPXMFtKi2bdIg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@types/hoist-non-react-statics": "^3.3.6", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.6.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "i18next": ">= 23.7.13", + "next": ">= 12.0.0", + "react": ">= 17.0.2", + "react-i18next": ">= 13.5.0" + } + }, + "node_modules/next/node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz", + "integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-arborist": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.4.3.tgz", + "integrity": "sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ==", + "license": "MIT", + "dependencies": { + "react-dnd": "^14.0.3", + "react-dnd-html5-backend": "^14.0.3", + "react-window": "^1.8.11", + "redux": "^5.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": ">= 16.14", + "react-dom": ">= 16.14" + } + }, + "node_modules/react-aria": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.39.0.tgz", + "integrity": "sha512-zXCjR01WnfW4uW0f294uWrvdfwEMHgDFSwMwMBwRafAvmsQea87X5VTAfDmQOAbPa+iQFcngIyH0Pn5CfXNrjw==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/string": "^3.2.6", + "@react-aria/breadcrumbs": "^3.5.23", + "@react-aria/button": "^3.13.0", + "@react-aria/calendar": "^3.8.0", + "@react-aria/checkbox": "^3.15.4", + "@react-aria/color": "^3.0.6", + "@react-aria/combobox": "^3.12.2", + "@react-aria/datepicker": "^3.14.2", + "@react-aria/dialog": "^3.5.24", + "@react-aria/disclosure": "^3.0.4", + "@react-aria/dnd": "^3.9.2", + "@react-aria/focus": "^3.20.2", + "@react-aria/gridlist": "^3.12.0", + "@react-aria/i18n": "^3.12.8", + "@react-aria/interactions": "^3.25.0", + "@react-aria/label": "^3.7.17", + "@react-aria/landmark": "^3.0.2", + "@react-aria/link": "^3.8.0", + "@react-aria/listbox": "^3.14.3", + "@react-aria/menu": "^3.18.2", + "@react-aria/meter": "^3.4.22", + "@react-aria/numberfield": "^3.11.13", + "@react-aria/overlays": "^3.27.0", + "@react-aria/progress": "^3.4.22", + "@react-aria/radio": "^3.11.2", + "@react-aria/searchfield": "^3.8.3", + "@react-aria/select": "^3.15.4", + "@react-aria/selection": "^3.24.0", + "@react-aria/separator": "^3.4.8", + "@react-aria/slider": "^3.7.18", + "@react-aria/ssr": "^3.9.8", + "@react-aria/switch": "^3.7.2", + "@react-aria/table": "^3.17.2", + "@react-aria/tabs": "^3.10.2", + "@react-aria/tag": "^3.5.2", + "@react-aria/textfield": "^3.17.2", + "@react-aria/toast": "^3.0.2", + "@react-aria/tooltip": "^3.8.2", + "@react-aria/tree": "^3.0.2", + "@react-aria/utils": "^3.28.2", + "@react-aria/visually-hidden": "^3.8.22", + "@react-types/shared": "^3.29.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-aria-components": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.8.0.tgz", + "integrity": "sha512-qNJ/Z4opj1/NKFf1ch/V8rNYar5MXu4J8YVAt2pFgnBRLjVlIlfnENN8Oa5OFiYFCzMPRFdq5mI8RuYIEnvZfg==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.8.0", + "@internationalized/string": "^3.2.6", + "@react-aria/autocomplete": "3.0.0-beta.2", + "@react-aria/collections": "3.0.0-rc.0", + "@react-aria/dnd": "^3.9.2", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@react-aria/live-announcer": "^3.4.2", + "@react-aria/overlays": "^3.27.0", + "@react-aria/ssr": "^3.9.8", + "@react-aria/toolbar": "3.0.0-beta.15", + "@react-aria/utils": "^3.28.2", + "@react-aria/virtualizer": "^4.1.4", + "@react-stately/autocomplete": "3.0.0-beta.1", + "@react-stately/layout": "^4.2.2", + "@react-stately/selection": "^3.20.1", + "@react-stately/table": "^3.14.1", + "@react-stately/utils": "^3.10.6", + "@react-stately/virtualizer": "^4.3.2", + "@react-types/form": "^3.7.11", + "@react-types/grid": "^3.3.1", + "@react-types/shared": "^3.29.0", + "@react-types/table": "^3.12.0", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "react-aria": "^3.39.0", + "react-stately": "^3.37.0", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-aria-components/node_modules/@react-aria/toolbar": { + "version": "3.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.15.tgz", + "integrity": "sha512-PNGpNIKIsCW8rxI9XXSADlLrSpikILJKKECyTRw9KwvXDRc44pezvdjGHCNinQcKsQoy5BtkK5cTSAyVqzzTXQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/focus": "^3.20.2", + "@react-aria/i18n": "^3.12.8", + "@react-aria/utils": "^3.28.2", + "@react-types/shared": "^3.29.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-dnd": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz", + "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz", + "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==", + "license": "MIT", + "dependencies": { + "dnd-core": "14.0.1" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-dropzone": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-15.0.0.tgz", + "integrity": "sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "16.5.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.6.tgz", + "integrity": "sha512-Ua7V2/efA88ido7KyK51fb8Ki8M/sRfW8LR/rZ/9ZKr2luhuTI7kwYZN5agT1rWG7aYm5G0RYE/6JR8KJoCMDw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", + "license": "MIT", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", + "integrity": "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-stately": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.37.0.tgz", + "integrity": "sha512-fm2LRM3XN5lJD48+WQKWvESx54kAIHw0JztCRHMsFmTDgYWX/VASuXKON7LECv227stSEadrxGa8LhPkcelljw==", + "license": "Apache-2.0", + "dependencies": { + "@react-stately/calendar": "^3.8.0", + "@react-stately/checkbox": "^3.6.13", + "@react-stately/collections": "^3.12.3", + "@react-stately/color": "^3.8.4", + "@react-stately/combobox": "^3.10.4", + "@react-stately/data": "^3.12.3", + "@react-stately/datepicker": "^3.14.0", + "@react-stately/disclosure": "^3.0.3", + "@react-stately/dnd": "^3.5.3", + "@react-stately/form": "^3.1.3", + "@react-stately/list": "^3.12.1", + "@react-stately/menu": "^3.9.3", + "@react-stately/numberfield": "^3.9.11", + "@react-stately/overlays": "^3.6.15", + "@react-stately/radio": "^3.10.12", + "@react-stately/searchfield": "^3.5.11", + "@react-stately/select": "^3.6.12", + "@react-stately/selection": "^3.20.1", + "@react-stately/slider": "^3.6.3", + "@react-stately/table": "^3.14.1", + "@react-stately/tabs": "^3.8.1", + "@react-stately/toast": "^3.1.0", + "@react-stately/toggle": "^3.8.3", + "@react-stately/tooltip": "^3.5.3", + "@react-stately/tree": "^3.8.9", + "@react-types/shared": "^3.29.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/react-window": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", + "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.53.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz", + "integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.3", + "esm-env": "^1.2.1", + "esrap": "^2.2.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte/node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-ics": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/ts-ics/-/ts-ics-2.4.2.tgz", + "integrity": "sha512-wbrFDku/Vc7xx89zuIflPUpFvTFoL3vH9upM/KTPeI1GQ66zscg48v4vpfR6iwkkR+72T5F57l66ApiAbNitjw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsdav": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/tsdav/-/tsdav-2.1.8.tgz", + "integrity": "sha512-zvQvhZLzTaEmNNgJbBlUYT/JOq9Xpw/xkxCqs7IT2d2/7o7pss0iZOlZXuHJ5VcvSvTny42Vc6+6GyzZcrCJ1g==", + "license": "MIT", + "dependencies": { + "base-64": "1.0.0", + "cross-fetch": "4.1.0", + "debug": "4.4.3", + "xml-js": "1.6.11" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/src/frontend/package.json b/src/frontend/package.json index 5540344..27cd710 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -2,24 +2,16 @@ "name": "main", "version": "0.10.1", "private": true, - "workspaces": [ - "apps/*", - "packages/*" - ], "engines": { - "node": ">=22.0.0 <25.0.0", - "yarn": "1.22.22" + "node": ">=24.0.0 <25.0.0" }, + "workspaces": [ + "apps/*" + ], "scripts": { - "dev": "yarn workspace calendars run dev", - "build": "yarn workspace calendars run build", - "lint": "yarn workspace calendars run lint", - "build-theme": "yarn workspace calendars build-theme" - }, - "resolutions": {}, - "devDependencies": { - "@types/minimatch": "^6.0.0", - "turbo": "2.6.1" - }, - "packageManager": "yarn@1.22.22" + "dev": "npm run dev -w calendars", + "build": "npm run build -w calendars", + "lint": "npm run lint -w calendars", + "build-theme": "npm run build-theme -w calendars" + } } diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock deleted file mode 100644 index 6eef404..0000000 --- a/src/frontend/yarn.lock +++ /dev/null @@ -1,7298 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7" - integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== - dependencies: - "@babel/helper-validator-identifier" "^7.28.5" - js-tokens "^4.0.0" - picocolors "^1.1.1" - -"@babel/compat-data@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.6.tgz#103f466803fa0f059e82ccac271475470570d74c" - integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== - -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.6.tgz#531bf883a1126e53501ba46eb3bb414047af507f" - integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw== - dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/generator" "^7.28.6" - "@babel/helper-compilation-targets" "^7.28.6" - "@babel/helper-module-transforms" "^7.28.6" - "@babel/helpers" "^7.28.6" - "@babel/parser" "^7.28.6" - "@babel/template" "^7.28.6" - "@babel/traverse" "^7.28.6" - "@babel/types" "^7.28.6" - "@jridgewell/remapping" "^2.3.5" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.28.6", "@babel/generator@^7.7.2": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" - integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== - dependencies: - "@babel/parser" "^7.28.6" - "@babel/types" "^7.28.6" - "@jridgewell/gen-mapping" "^0.3.12" - "@jridgewell/trace-mapping" "^0.3.28" - jsesc "^3.0.2" - -"@babel/helper-compilation-targets@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" - integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== - dependencies: - "@babel/compat-data" "^7.28.6" - "@babel/helper-validator-option" "^7.27.1" - browserslist "^4.24.0" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-globals@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" - integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== - -"@babel/helper-module-imports@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" - integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== - dependencies: - "@babel/traverse" "^7.28.6" - "@babel/types" "^7.28.6" - -"@babel/helper-module-transforms@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" - integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== - dependencies: - "@babel/helper-module-imports" "^7.28.6" - "@babel/helper-validator-identifier" "^7.28.5" - "@babel/traverse" "^7.28.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.28.6", "@babel/helper-plugin-utils@^7.8.0": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" - integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== - -"@babel/helper-string-parser@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" - integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== - -"@babel/helper-validator-identifier@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" - integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== - -"@babel/helper-validator-option@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" - integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== - -"@babel/helpers@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" - integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== - dependencies: - "@babel/template" "^7.28.6" - "@babel/types" "^7.28.6" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" - integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== - dependencies: - "@babel/types" "^7.28.6" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-import-attributes@^7.24.7": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz#b71d5914665f60124e133696f17cd7669062c503" - integrity sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw== - dependencies: - "@babel/helper-plugin-utils" "^7.28.6" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.7.2": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz#f8ca28bbd84883b5fea0e447c635b81ba73997ee" - integrity sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w== - dependencies: - "@babel/helper-plugin-utils" "^7.28.6" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.7.2": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz#c7b2ddf1d0a811145b1de800d1abd146af92e3a2" - integrity sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A== - dependencies: - "@babel/helper-plugin-utils" "^7.28.6" - -"@babel/runtime@^7.0.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.5", "@babel/runtime@^7.27.6", "@babel/runtime@^7.9.2": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" - integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== - -"@babel/template@^7.28.6", "@babel/template@^7.3.3": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" - integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== - dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/parser" "^7.28.6" - "@babel/types" "^7.28.6" - -"@babel/traverse@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" - integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== - dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/generator" "^7.28.6" - "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.6" - "@babel/template" "^7.28.6" - "@babel/types" "^7.28.6" - debug "^4.3.1" - -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.3.3": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" - integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== - dependencies: - "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.28.5" - -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@dnd-kit/accessibility@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af" - integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw== - dependencies: - tslib "^2.0.0" - -"@dnd-kit/core@6.3.1": - version "6.3.1" - resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003" - integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ== - dependencies: - "@dnd-kit/accessibility" "^3.1.1" - "@dnd-kit/utilities" "^3.2.2" - tslib "^2.0.0" - -"@dnd-kit/modifiers@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz#96a0280c77b10c716ef79d9792ce7ad04370771d" - integrity sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw== - dependencies: - "@dnd-kit/utilities" "^3.2.2" - tslib "^2.0.0" - -"@dnd-kit/sortable@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8" - integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg== - dependencies: - "@dnd-kit/utilities" "^3.2.2" - tslib "^2.0.0" - -"@dnd-kit/utilities@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b" - integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg== - dependencies: - tslib "^2.0.0" - -"@emnapi/core@^1.4.3": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.8.1.tgz#fd9efe721a616288345ffee17a1f26ac5dd01349" - integrity sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg== - dependencies: - "@emnapi/wasi-threads" "1.1.0" - tslib "^2.4.0" - -"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.7.0": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5" - integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg== - dependencies: - tslib "^2.4.0" - -"@emnapi/wasi-threads@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" - integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== - dependencies: - tslib "^2.4.0" - -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" - integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== - dependencies: - eslint-visitor-keys "^3.4.3" - -"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2": - version "4.12.2" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" - integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== - -"@eslint/config-array@^0.19.0": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa" - integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w== - dependencies: - "@eslint/object-schema" "^2.1.6" - debug "^4.3.1" - minimatch "^3.1.2" - -"@eslint/core@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.11.0.tgz#7a9226e850922e42cbd2ba71361eacbe74352a12" - integrity sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA== - dependencies: - "@types/json-schema" "^7.0.15" - -"@eslint/core@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.13.0.tgz#bf02f209846d3bf996f9e8009db62df2739b458c" - integrity sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw== - dependencies: - "@types/json-schema" "^7.0.15" - -"@eslint/eslintrc@3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c" - integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^10.0.1" - globals "^14.0.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/eslintrc@^3.2.0": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac" - integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^10.0.1" - globals "^14.0.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.1" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/js@9.20.0": - version "9.20.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.20.0.tgz#7421bcbe74889fcd65d1be59f00130c289856eb4" - integrity sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ== - -"@eslint/object-schema@^2.1.6": - version "2.1.7" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" - integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== - -"@eslint/plugin-kit@^0.2.5": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8" - integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA== - dependencies: - "@eslint/core" "^0.13.0" - levn "^0.4.1" - -"@event-calendar/core@^5.2.3": - version "5.2.4" - resolved "https://registry.yarnpkg.com/@event-calendar/core/-/core-5.2.4.tgz#ba79239b96d42864e0e944c64a5a5c146b626094" - integrity sha512-k2CD2ehm6RM3bG871MdYBxlX6TpVTnrFq0pzLhrWPxeO7iF3iEoWRnG7ik+ZHkelu3bIxWMeMlO9MGZi0BjZdQ== - dependencies: - svelte "^5.47.1" - -"@fontsource-variable/roboto-flex@5.2.5": - version "5.2.5" - resolved "https://registry.yarnpkg.com/@fontsource-variable/roboto-flex/-/roboto-flex-5.2.5.tgz#38368ea754697c2fdf08df11b06e8b6d391ff4c1" - integrity sha512-yrZ9rWNvfM4IBqJSpoFV4wC1GaKNwpzhihyBke+N3WiA0Y7LYxwj6kvvHgecabVIHFL3ZZJTwj3KcKOCyASFPg== - -"@fontsource/material-icons-outlined@5.2.5": - version "5.2.5" - resolved "https://registry.yarnpkg.com/@fontsource/material-icons-outlined/-/material-icons-outlined-5.2.5.tgz#16b4b2a8dcd1fbd69d4182792a65f7b41b8cb230" - integrity sha512-soAUWorSKrLN0a7wk74pedV0ZxhLaMD40DuUTMIqTpDcdJ/pxaG+/OcXtMEzx2+5K5FwL7yS525ZInAzPG051Q== - -"@fontsource/material-icons@5.2.5": - version "5.2.5" - resolved "https://registry.yarnpkg.com/@fontsource/material-icons/-/material-icons-5.2.5.tgz#cdb9dd23c0da4b021c866dc529ed4c203414ec3c" - integrity sha512-9k0LBRVgResIeD+vC/epYmm/awN2k2L8twwEtUWQ3FHluMi+7PbISOpXqksvfqPn9FJy4/KEeWOhFTR/SrbhNw== - -"@formatjs/ecma402-abstract@2.3.6": - version "2.3.6" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz#d6ca9d3579054fe1e1a0a0b5e872e0d64922e4e1" - integrity sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw== - dependencies: - "@formatjs/fast-memoize" "2.2.7" - "@formatjs/intl-localematcher" "0.6.2" - decimal.js "^10.4.3" - tslib "^2.8.0" - -"@formatjs/fast-memoize@2.2.7": - version "2.2.7" - resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz#707f9ddaeb522a32f6715bb7950b0831f4cc7b15" - integrity sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ== - dependencies: - tslib "^2.8.0" - -"@formatjs/icu-messageformat-parser@2.11.4": - version "2.11.4" - resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz#63bd2cd82d08ae2bef55adeeb86486df68826f32" - integrity sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw== - dependencies: - "@formatjs/ecma402-abstract" "2.3.6" - "@formatjs/icu-skeleton-parser" "1.8.16" - tslib "^2.8.0" - -"@formatjs/icu-skeleton-parser@1.8.16": - version "1.8.16" - resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz#13f81f6845c7cf6599623006aacaf7d6b4ad2970" - integrity sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ== - dependencies: - "@formatjs/ecma402-abstract" "2.3.6" - tslib "^2.8.0" - -"@formatjs/intl-localematcher@0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz#e9ebe0b4082d7d48e5b2d753579fb7ece4eaefea" - integrity sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA== - dependencies: - tslib "^2.8.0" - -"@gouvfr-lasuite/cunningham-react@4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/cunningham-react/-/cunningham-react-4.2.0.tgz#19bc8a658cc069d7b9397cba735b194e578b9b1f" - integrity sha512-97eTA+v/ySsl6d5NCn6D2mVoNqrJnHWG+PWCY1L/Fz8MEx18sPwqUsqdaIuSj0C+iug0SCJ6Ufx57MURHCf0oA== - dependencies: - "@fontsource-variable/roboto-flex" "5.2.5" - "@fontsource/material-icons-outlined" "5.2.5" - "@gouvfr-lasuite/cunningham-tokens" "*" - "@internationalized/date" "3.8.0" - "@react-aria/calendar" "3.8.0" - "@react-aria/datepicker" "3.14.2" - "@react-aria/i18n" "3.12.8" - "@react-stately/calendar" "3.8.0" - "@react-stately/datepicker" "3.14.0" - "@tanstack/react-table" "8.21.3" - "@types/react-modal" "3.16.3" - chromatic "11.28.2" - classnames "2.5.1" - downshift "9.0.9" - react-aria "3.39.0" - react-aria-components "1.8.0" - react-modal "3.16.3" - react-stately "3.37.0" - -"@gouvfr-lasuite/cunningham-tokens@*", "@gouvfr-lasuite/cunningham-tokens@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/cunningham-tokens/-/cunningham-tokens-3.1.0.tgz#39d957e6322f7c10b463dc8eeed8fdbb3e006788" - integrity sha512-7HP7E1vjxv3FbFn21XuEqbjQPHP5xucWU0D8+Oj02nIeKLL+0eArTL2GB+c2rPFBt0tAzaLI6A3WlsimOjHN4A== - dependencies: - chalk "4.1.2" - commander "13.1.0" - deepmerge "4.3.1" - figlet "1.8.1" - ts-node "10.9.2" - -"@gouvfr-lasuite/integration@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/integration/-/integration-1.0.2.tgz#ed0000f4b738c5a19bb60f5b80a9a2f5d9414234" - integrity sha512-npOotZQSyu6SffHiPP+jQVOkJ3qW2KE2cANhEK92sNLX9uZqQaCqljO5GhzsBmh0lB76fiXnrr9i8SIpnDUSZg== - -"@gouvfr-lasuite/ui-kit@0.19.6": - version "0.19.6" - resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/ui-kit/-/ui-kit-0.19.6.tgz#bc9e32f5b575147bd92e19172aaac794522c4772" - integrity sha512-PY0Jh2Wno/+dBVy89OdAook3h9GfrJXXVyOeNLA5mJ9NlNQBq0XKxSB87iqzDn/CBe1aUSLJY47QWzRWQQ0wXQ== - dependencies: - "@dnd-kit/core" "6.3.1" - "@dnd-kit/modifiers" "9.0.0" - "@dnd-kit/sortable" "10.0.0" - "@fontsource/material-icons" "5.2.5" - "@gouvfr-lasuite/cunningham-react" "4.2.0" - "@gouvfr-lasuite/cunningham-tokens" "3.1.0" - "@gouvfr-lasuite/integration" "1.0.2" - "@types/node" "22.10.7" - clsx "2.1.1" - cmdk "1.0.4" - react-arborist "3.4.3" - react-aria-components "1.8.0" - react-resizable-panels "2.1.7" - react-stately "3.37.0" - -"@humanfs/core@^0.19.1": - version "0.19.1" - resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" - integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== - -"@humanfs/node@^0.16.6": - version "0.16.7" - resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" - integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== - dependencies: - "@humanfs/core" "^0.19.1" - "@humanwhocodes/retry" "^0.4.0" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.1": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" - integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== - -"@img/colour@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311" - integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw== - -"@img/sharp-darwin-arm64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86" - integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.2.4" - -"@img/sharp-darwin-x64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b" - integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.2.4" - -"@img/sharp-libvips-darwin-arm64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43" - integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== - -"@img/sharp-libvips-darwin-x64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc" - integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg== - -"@img/sharp-libvips-linux-arm64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318" - integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw== - -"@img/sharp-libvips-linux-arm@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d" - integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A== - -"@img/sharp-libvips-linux-ppc64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7" - integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA== - -"@img/sharp-libvips-linux-riscv64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de" - integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA== - -"@img/sharp-libvips-linux-s390x@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec" - integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ== - -"@img/sharp-libvips-linux-x64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce" - integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw== - -"@img/sharp-libvips-linuxmusl-arm64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06" - integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw== - -"@img/sharp-libvips-linuxmusl-x64@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75" - integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg== - -"@img/sharp-linux-arm64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc" - integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.2.4" - -"@img/sharp-linux-arm@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d" - integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.2.4" - -"@img/sharp-linux-ppc64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813" - integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA== - optionalDependencies: - "@img/sharp-libvips-linux-ppc64" "1.2.4" - -"@img/sharp-linux-riscv64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60" - integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw== - optionalDependencies: - "@img/sharp-libvips-linux-riscv64" "1.2.4" - -"@img/sharp-linux-s390x@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7" - integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.2.4" - -"@img/sharp-linux-x64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8" - integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ== - optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.2.4" - -"@img/sharp-linuxmusl-arm64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086" - integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" - -"@img/sharp-linuxmusl-x64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f" - integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.2.4" - -"@img/sharp-wasm32@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0" - integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw== - dependencies: - "@emnapi/runtime" "^1.7.0" - -"@img/sharp-win32-arm64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a" - integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g== - -"@img/sharp-win32-ia32@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de" - integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg== - -"@img/sharp-win32-x64@0.34.5": - version "0.34.5" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" - integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== - -"@internationalized/date@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.8.0.tgz#24fb301029224351381aa87cba853ca1093af094" - integrity sha512-J51AJ0fEL68hE4CwGPa6E0PO6JDaVLd8aln48xFCSy7CZkZc96dGEGmLs2OEEbBxcsVZtfrqkXJwI2/MSG8yKw== - dependencies: - "@swc/helpers" "^0.5.0" - -"@internationalized/date@^3.10.1", "@internationalized/date@^3.8.0": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.10.1.tgz#ca63817feadeffe97f710289b00af229cd8af15c" - integrity sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA== - dependencies: - "@swc/helpers" "^0.5.0" - -"@internationalized/message@^3.1.7", "@internationalized/message@^3.1.8": - version "3.1.8" - resolved "https://registry.yarnpkg.com/@internationalized/message/-/message-3.1.8.tgz#7181e8178f0868535f4507a573bf285e925832cb" - integrity sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA== - dependencies: - "@swc/helpers" "^0.5.0" - intl-messageformat "^10.1.0" - -"@internationalized/number@^3.6.1", "@internationalized/number@^3.6.5": - version "3.6.5" - resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.5.tgz#1103f2832ca8d9dd3e4eecf95733d497791dbbbe" - integrity sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g== - dependencies: - "@swc/helpers" "^0.5.0" - -"@internationalized/string@^3.2.6", "@internationalized/string@^3.2.7": - version "3.2.7" - resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.7.tgz#76ae10f1e6e1fdaec7d0028a3f807d37a71bd2dd" - integrity sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A== - dependencies: - "@swc/helpers" "^0.5.0" - -"@isaacs/balanced-match@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" - integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== - -"@isaacs/brace-expansion@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" - integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== - dependencies: - "@isaacs/balanced-match" "^4.0.1" - -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - dependencies: - camelcase "^5.3.1" - find-up "^4.1.0" - get-package-type "^0.1.0" - js-yaml "^3.13.1" - resolve-from "^5.0.0" - -"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/console@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" - integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - -"@jest/core@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" - integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== - dependencies: - "@jest/console" "^29.7.0" - "@jest/reporters" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - ci-info "^3.2.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-changed-files "^29.7.0" - jest-config "^29.7.0" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-resolve-dependencies "^29.7.0" - jest-runner "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - jest-watcher "^29.7.0" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - -"@jest/environment@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" - integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== - dependencies: - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - -"@jest/expect-utils@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" - integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== - dependencies: - jest-get-type "^29.6.3" - -"@jest/expect@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" - integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== - dependencies: - expect "^29.7.0" - jest-snapshot "^29.7.0" - -"@jest/fake-timers@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" - integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== - dependencies: - "@jest/types" "^29.6.3" - "@sinonjs/fake-timers" "^10.0.2" - "@types/node" "*" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-util "^29.7.0" - -"@jest/globals@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" - integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/types" "^29.6.3" - jest-mock "^29.7.0" - -"@jest/reporters@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" - integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - "@types/node" "*" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^6.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.1.3" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - jest-worker "^29.7.0" - slash "^3.0.0" - string-length "^4.0.1" - strip-ansi "^6.0.0" - v8-to-istanbul "^9.0.1" - -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - -"@jest/source-map@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" - integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.18" - callsites "^3.0.0" - graceful-fs "^4.2.9" - -"@jest/test-result@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" - integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== - dependencies: - "@jest/console" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-sequencer@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" - integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== - dependencies: - "@jest/test-result" "^29.7.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - slash "^3.0.0" - -"@jest/transform@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" - integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.2" - -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== - dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.13" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" - integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/remapping@^2.3.4", "@jridgewell/remapping@^2.3.5": - version "2.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" - integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" - integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": - version "0.3.31" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" - integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@napi-rs/wasm-runtime@^0.2.11": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" - integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== - dependencies: - "@emnapi/core" "^1.4.3" - "@emnapi/runtime" "^1.4.3" - "@tybys/wasm-util" "^0.10.0" - -"@next/env@15.4.9": - version "15.4.9" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.4.9.tgz#5caeeaf5f20cf2bf5176444f13d087963159031f" - integrity sha512-OYR0RulK5phnbxxzcLE4/ECgfx1PL3EHrDbjyelJ7jauaO+/Qvj5gG8JPMltB51CygC2KrZ0aAfYLjPYjYY17A== - -"@next/eslint-plugin-next@15.1.7": - version "15.1.7" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.7.tgz#f8593c714f51ac6d443fb298584954d5def4392b" - integrity sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ== - dependencies: - fast-glob "3.3.1" - -"@next/swc-darwin-arm64@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.8.tgz#f5030219421079036720b5948ea9de9bee02ac34" - integrity sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A== - -"@next/swc-darwin-x64@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.8.tgz#3fc796bd522aee30eff608448919e0ea1c1da7d1" - integrity sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw== - -"@next/swc-linux-arm64-gnu@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.8.tgz#5c7bf6a0de49c2b4c21bc8bdb3b5e431dd922c7d" - integrity sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw== - -"@next/swc-linux-arm64-musl@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.8.tgz#94e47838715e68a696b33295f4389a60bd8af00a" - integrity sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A== - -"@next/swc-linux-x64-gnu@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.8.tgz#4f9656e8bf9f28dac1970d6ff95a245014646417" - integrity sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA== - -"@next/swc-linux-x64-musl@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.8.tgz#c5c64e18370f54f6474e58bb01b12594a4ecdde6" - integrity sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA== - -"@next/swc-win32-arm64-msvc@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.8.tgz#722d18ed569bee9e4a6651acdc756f9633cbee1f" - integrity sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw== - -"@next/swc-win32-x64-msvc@15.4.8": - version "15.4.8" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.8.tgz#a29a53cd262ec5093b9ac24a5fd5e4540ec64eb4" - integrity sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@nolyfill/is-core-module@1.0.39": - version "1.0.39" - resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" - integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== - -"@parcel/watcher-android-arm64@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.4.tgz#88c67bde2c3efa997a0b1fea540080c6ade0322c" - integrity sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g== - -"@parcel/watcher-darwin-arm64@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.4.tgz#d9dc037cff8a4ab7839a79c5287a6e6660f7ab27" - integrity sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw== - -"@parcel/watcher-darwin-x64@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.4.tgz#da0e13e16ee6d378242e2cfb469d72667624383a" - integrity sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg== - -"@parcel/watcher-freebsd-x64@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.4.tgz#feb7cc9ec680bae3e91dddcdb4fe1c399ed52cc1" - integrity sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q== - -"@parcel/watcher-linux-arm-glibc@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.4.tgz#fa4e9cf8228c8c433e2f035e8b16aa299d892a78" - integrity sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw== - -"@parcel/watcher-linux-arm-musl@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.4.tgz#9ee6792e2d8810af9871ee5bbc2aa04e0b079d62" - integrity sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ== - -"@parcel/watcher-linux-arm64-glibc@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.4.tgz#624c6d874d99afa79305720f96a0c233d4ad7fde" - integrity sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw== - -"@parcel/watcher-linux-arm64-musl@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.4.tgz#5341e88b9e645d31c015ed40f384e60e49bd74d2" - integrity sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ== - -"@parcel/watcher-linux-x64-glibc@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.4.tgz#be5bcc49d3f6d21cc81bb531970a05d3721e385c" - integrity sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA== - -"@parcel/watcher-linux-x64-musl@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.4.tgz#bffd3895b1f0cc8fd1436e409fd65d0a901281c0" - integrity sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg== - -"@parcel/watcher-win32-arm64@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.4.tgz#7fb8aedea5b34ba97a01e1555929d01f4eb72fe4" - integrity sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ== - -"@parcel/watcher-win32-ia32@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.4.tgz#f7f94ebdb21dedf37b12e030a82d4211798a1c26" - integrity sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg== - -"@parcel/watcher-win32-x64@2.5.4": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.4.tgz#8d895c9723f7fffdf4b360fd1becf1b6bcb571df" - integrity sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw== - -"@parcel/watcher@^2.4.1": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.4.tgz#a6575b0a018b4e263589c1e7bc2ceb73c1ee84de" - integrity sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ== - dependencies: - detect-libc "^2.0.3" - is-glob "^4.0.3" - node-addon-api "^7.0.0" - picomatch "^4.0.3" - optionalDependencies: - "@parcel/watcher-android-arm64" "2.5.4" - "@parcel/watcher-darwin-arm64" "2.5.4" - "@parcel/watcher-darwin-x64" "2.5.4" - "@parcel/watcher-freebsd-x64" "2.5.4" - "@parcel/watcher-linux-arm-glibc" "2.5.4" - "@parcel/watcher-linux-arm-musl" "2.5.4" - "@parcel/watcher-linux-arm64-glibc" "2.5.4" - "@parcel/watcher-linux-arm64-musl" "2.5.4" - "@parcel/watcher-linux-x64-glibc" "2.5.4" - "@parcel/watcher-linux-x64-musl" "2.5.4" - "@parcel/watcher-win32-arm64" "2.5.4" - "@parcel/watcher-win32-ia32" "2.5.4" - "@parcel/watcher-win32-x64" "2.5.4" - -"@playwright/test@1.56.1": - version "1.56.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" - integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== - dependencies: - playwright "1.56.1" - -"@radix-ui/primitive@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba" - integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg== - -"@radix-ui/react-compose-refs@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" - integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== - -"@radix-ui/react-context@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" - integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== - -"@radix-ui/react-dialog@^1.1.2": - version "1.1.15" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632" - integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-context" "1.1.2" - "@radix-ui/react-dismissable-layer" "1.1.11" - "@radix-ui/react-focus-guards" "1.1.3" - "@radix-ui/react-focus-scope" "1.1.7" - "@radix-ui/react-id" "1.1.1" - "@radix-ui/react-portal" "1.1.9" - "@radix-ui/react-presence" "1.1.5" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-slot" "1.2.3" - "@radix-ui/react-use-controllable-state" "1.2.2" - aria-hidden "^1.2.4" - react-remove-scroll "^2.6.3" - -"@radix-ui/react-dismissable-layer@1.1.11": - version "1.1.11" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37" - integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg== - dependencies: - "@radix-ui/primitive" "1.1.3" - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - "@radix-ui/react-use-escape-keydown" "1.1.1" - -"@radix-ui/react-focus-guards@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f" - integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw== - -"@radix-ui/react-focus-scope@1.1.7": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d" - integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-callback-ref" "1.1.1" - -"@radix-ui/react-id@1.1.1", "@radix-ui/react-id@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" - integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== - dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-portal@1.1.9": - version "1.1.9" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" - integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== - dependencies: - "@radix-ui/react-primitive" "2.1.3" - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-presence@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db" - integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-primitive@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" - integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== - dependencies: - "@radix-ui/react-slot" "1.2.3" - -"@radix-ui/react-primitive@^2.0.0": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz#2626ea309ebd63bf5767d3e7fc4081f81b993df0" - integrity sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg== - dependencies: - "@radix-ui/react-slot" "1.2.4" - -"@radix-ui/react-slot@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" - integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - -"@radix-ui/react-slot@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz#63c0ba05fdf90cc49076b94029c852d7bac1fb83" - integrity sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - -"@radix-ui/react-use-callback-ref@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" - integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== - -"@radix-ui/react-use-controllable-state@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" - integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== - dependencies: - "@radix-ui/react-use-effect-event" "0.0.2" - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-use-effect-event@0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" - integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== - dependencies: - "@radix-ui/react-use-layout-effect" "1.1.1" - -"@radix-ui/react-use-escape-keydown@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" - integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== - dependencies: - "@radix-ui/react-use-callback-ref" "1.1.1" - -"@radix-ui/react-use-layout-effect@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" - integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== - -"@react-aria/autocomplete@3.0.0-beta.2": - version "3.0.0-beta.2" - resolved "https://registry.yarnpkg.com/@react-aria/autocomplete/-/autocomplete-3.0.0-beta.2.tgz#61fc21cde05206e9a90f32378f25d13f5424366c" - integrity sha512-oxsFCIGj5yooQkZzdqjvsdfr9fOlmAq4v6njIOAyQFsta3H0yQiv+YU3XnrnCBxVX+Mz/mZtZgfhAA9JBDukHg== - dependencies: - "@react-aria/combobox" "^3.12.2" - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/listbox" "^3.14.3" - "@react-aria/searchfield" "^3.8.3" - "@react-aria/textfield" "^3.17.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/autocomplete" "3.0.0-beta.1" - "@react-stately/combobox" "^3.10.4" - "@react-types/autocomplete" "3.0.0-alpha.30" - "@react-types/button" "^3.12.0" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-aria/breadcrumbs@^3.5.23", "@react-aria/breadcrumbs@^3.5.30": - version "3.5.30" - resolved "https://registry.yarnpkg.com/@react-aria/breadcrumbs/-/breadcrumbs-3.5.30.tgz#4ae1b63bf263cfc1413a8700f1c533828feb74c0" - integrity sha512-DZymglA70SwvDJA7GB147sUexvdDy6vWcriGrlEHhMMzBLhGB30I5J96R4pPzURLxXISrWFH56KC5rRgIqsqqg== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/link" "^3.8.7" - "@react-aria/utils" "^3.32.0" - "@react-types/breadcrumbs" "^3.7.17" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/button@^3.13.0", "@react-aria/button@^3.14.3": - version "3.14.3" - resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.14.3.tgz#366e84224398c57cc63540bb4d05d809939e2561" - integrity sha512-iJTuEECs9im7TwrCRZ0dvuwp8Gao0+I1IuYs1LQvJQgKLpgRH2/6jAiqb2bdAcoAjdbaMs7Xe0xUwURpVNkEyA== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/toolbar" "3.0.0-beta.22" - "@react-aria/utils" "^3.32.0" - "@react-stately/toggle" "^3.9.3" - "@react-types/button" "^3.14.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/calendar@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@react-aria/calendar/-/calendar-3.8.0.tgz#8f210d2ecfd89a8020f5568fdf2014a8e6c62a46" - integrity sha512-9vms/fWjJPZkJcMxciwWWOjGy/Q0nqI6FV0pYbMZbqepkzglEaVd98kl506r/4hLhWKwLdTfqCgbntRecj8jBg== - dependencies: - "@internationalized/date" "^3.8.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/calendar" "^3.8.0" - "@react-types/button" "^3.12.0" - "@react-types/calendar" "^3.7.0" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-aria/calendar@^3.8.0", "@react-aria/calendar@^3.9.3": - version "3.9.3" - resolved "https://registry.yarnpkg.com/@react-aria/calendar/-/calendar-3.9.3.tgz#6b1f73a0f8a2a2959772cc11fdb1084545b04c31" - integrity sha512-F12UQ4zd8GIxpJxs9GAHzDD9Lby2hESHm0LF5tjsYBIOBJc5K7ICeeE5UqLMBPzgnEP5nfh1CKS8KhCB0mS7PA== - dependencies: - "@internationalized/date" "^3.10.1" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/live-announcer" "^3.4.4" - "@react-aria/utils" "^3.32.0" - "@react-stately/calendar" "^3.9.1" - "@react-types/button" "^3.14.1" - "@react-types/calendar" "^3.8.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/checkbox@^3.15.4", "@react-aria/checkbox@^3.16.3": - version "3.16.3" - resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.16.3.tgz#b48f90c0baada06b22ca63dea8214fe98dc21c15" - integrity sha512-2p1haCUtERo5XavBAWNaX//dryNVnOOWfSKyzLs4UiCZR/NL0ttN+Nu/i445q0ipjLqZ6bBJtx0g0NNrubbU7Q== - dependencies: - "@react-aria/form" "^3.1.3" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/toggle" "^3.12.3" - "@react-aria/utils" "^3.32.0" - "@react-stately/checkbox" "^3.7.3" - "@react-stately/form" "^3.2.2" - "@react-stately/toggle" "^3.9.3" - "@react-types/checkbox" "^3.10.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/collections@3.0.0-rc.0": - version "3.0.0-rc.0" - resolved "https://registry.yarnpkg.com/@react-aria/collections/-/collections-3.0.0-rc.0.tgz#d0a906f4a4582a91b6ad866a58fbe2ef1713b15e" - integrity sha512-WcRcE3wKtbprOJlBaMbdYS5Suu2KIGq1gVT2fLXVbmDY0CjGemqp2m5aDblQOO8pxvsAqHV8pyznkhANTnK1CQ== - dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/ssr" "^3.9.8" - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - use-sync-external-store "^1.4.0" - -"@react-aria/color@^3.0.6", "@react-aria/color@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@react-aria/color/-/color-3.1.3.tgz#a20d214ae522015043cdcd0d49cec913a998283d" - integrity sha512-EHzsFbqzFrO1/3irEa8E8wawlQg7hRd4/Jscvl9zhplAcrWFd6L5TWl8463Z6h0J6zN1eH9T2QDEn6rivDLkkg== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/numberfield" "^3.12.3" - "@react-aria/slider" "^3.8.3" - "@react-aria/spinbutton" "^3.7.0" - "@react-aria/textfield" "^3.18.3" - "@react-aria/utils" "^3.32.0" - "@react-aria/visually-hidden" "^3.8.29" - "@react-stately/color" "^3.9.3" - "@react-stately/form" "^3.2.2" - "@react-types/color" "^3.1.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/combobox@^3.12.2", "@react-aria/combobox@^3.14.1": - version "3.14.1" - resolved "https://registry.yarnpkg.com/@react-aria/combobox/-/combobox-3.14.1.tgz#c6fb3ea420860547064602103de2dc489122b2e0" - integrity sha512-wuP/4UQrGsYXLw1Gk8G/FcnUlHuoViA9G6w3LhtUgu5Q3E5DvASJalxej3NtyYU+4w4epD1gJidzosAL0rf8Ug== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/listbox" "^3.15.1" - "@react-aria/live-announcer" "^3.4.4" - "@react-aria/menu" "^3.19.4" - "@react-aria/overlays" "^3.31.0" - "@react-aria/selection" "^3.27.0" - "@react-aria/textfield" "^3.18.3" - "@react-aria/utils" "^3.32.0" - "@react-stately/collections" "^3.12.8" - "@react-stately/combobox" "^3.12.1" - "@react-stately/form" "^3.2.2" - "@react-types/button" "^3.14.1" - "@react-types/combobox" "^3.13.10" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/datepicker@3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@react-aria/datepicker/-/datepicker-3.14.2.tgz#db1efa94edc86b57950cf32cbfd252b206653fab" - integrity sha512-O7fdzcqIJ7i/+8SGYvx4tloTZgK4Ws8OChdbFcd2rZoRPqxM50M6J+Ota8hTet2wIhojUXnM3x2na3EvoucBXA== - dependencies: - "@internationalized/date" "^3.8.0" - "@internationalized/number" "^3.6.1" - "@internationalized/string" "^3.2.6" - "@react-aria/focus" "^3.20.2" - "@react-aria/form" "^3.0.15" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/spinbutton" "^3.6.14" - "@react-aria/utils" "^3.28.2" - "@react-stately/datepicker" "^3.14.0" - "@react-stately/form" "^3.1.3" - "@react-types/button" "^3.12.0" - "@react-types/calendar" "^3.7.0" - "@react-types/datepicker" "^3.12.0" - "@react-types/dialog" "^3.5.17" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-aria/datepicker@^3.14.2", "@react-aria/datepicker@^3.15.3": - version "3.15.3" - resolved "https://registry.yarnpkg.com/@react-aria/datepicker/-/datepicker-3.15.3.tgz#fa5c15a8ef18d5a2d9cea1984993851eac1c5c9e" - integrity sha512-0KkLYeLs+IubHXb879n8dzzKU/NWcxC9DXtv7M/ofL7vAvMSTmaceYJcMW+2gGYhJVpyYz8B6bk0W7kTxgB3jg== - dependencies: - "@internationalized/date" "^3.10.1" - "@internationalized/number" "^3.6.5" - "@internationalized/string" "^3.2.7" - "@react-aria/focus" "^3.21.3" - "@react-aria/form" "^3.1.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/spinbutton" "^3.7.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/datepicker" "^3.15.3" - "@react-stately/form" "^3.2.2" - "@react-types/button" "^3.14.1" - "@react-types/calendar" "^3.8.1" - "@react-types/datepicker" "^3.13.3" - "@react-types/dialog" "^3.5.22" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/dialog@^3.5.24", "@react-aria/dialog@^3.5.32": - version "3.5.32" - resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.5.32.tgz#3013a8550192f3c37f8550d428ea5dacf21f0039" - integrity sha512-2puMjsJS2FtB8LiFuQDAdBSU4dt3lqdJn4FWt/8GL6l91RZBqp2Dnm5Obuee6rV2duNJZcSAUWsQZ/S1iW8Y2g== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/overlays" "^3.31.0" - "@react-aria/utils" "^3.32.0" - "@react-types/dialog" "^3.5.22" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/disclosure@^3.0.4", "@react-aria/disclosure@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@react-aria/disclosure/-/disclosure-3.1.1.tgz#065212c1ab60067ce860c349fb25452ce39e8578" - integrity sha512-4k8Y3CZEl+Qhou0fH7Sj7BbzvwAfi1JDL+hG7U20ZL5+MJ/VbDYuYX2gYK2KqdlbeuuzGcov3ZFQbyIVHMY+/A== - dependencies: - "@react-aria/ssr" "^3.9.10" - "@react-aria/utils" "^3.32.0" - "@react-stately/disclosure" "^3.0.9" - "@react-types/button" "^3.14.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/dnd@^3.11.4", "@react-aria/dnd@^3.9.2": - version "3.11.4" - resolved "https://registry.yarnpkg.com/@react-aria/dnd/-/dnd-3.11.4.tgz#88ffbb949282198bbddedb36bf2eadde7d747fb0" - integrity sha512-dBrnM33Kmk76F+Pknh2WfSLIX4dsYwFzWJUIABJCPmPc80hTG0so7mfqH45ba759/6ERMfXXoodZPLtypOjYPg== - dependencies: - "@internationalized/string" "^3.2.7" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/live-announcer" "^3.4.4" - "@react-aria/overlays" "^3.31.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/collections" "^3.12.8" - "@react-stately/dnd" "^3.7.2" - "@react-types/button" "^3.14.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/focus@^3.20.2", "@react-aria/focus@^3.21.3": - version "3.21.3" - resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.21.3.tgz#bdcdfc45735c785de5ba54cc907e531926dcc4c5" - integrity sha512-FsquWvjSCwC2/sBk4b+OqJyONETUIXQ2vM0YdPAuC+QFQh2DT6TIBo6dOZVSezlhudDla69xFBd6JvCFq1AbUw== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - clsx "^2.0.0" - -"@react-aria/form@^3.0.15", "@react-aria/form@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@react-aria/form/-/form-3.1.3.tgz#e75c328b44483c8397bf73106389f9ab08f56856" - integrity sha512-HAKnPjMiqTxoGLVbfZyGYcZQ1uu6aSeCi9ODmtZuKM5DWZZnTUjDmM1i2L6IXvF+d1kjyApyJC7VTbKZ8AI77g== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/form" "^3.2.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/grid@^3.14.6": - version "3.14.6" - resolved "https://registry.yarnpkg.com/@react-aria/grid/-/grid-3.14.6.tgz#055cac0c12a4e5219436882670536d172d105794" - integrity sha512-xagBKHNPu4Ovt/I5He7T/oIEq82MDMSrRi5Sw3oxSCwwtZpv+7eyKRSrFz9vrNUzNgWCcx5VHLE660bLdeVNDQ== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/live-announcer" "^3.4.4" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/collections" "^3.12.8" - "@react-stately/grid" "^3.11.7" - "@react-stately/selection" "^3.20.7" - "@react-types/checkbox" "^3.10.2" - "@react-types/grid" "^3.3.6" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/gridlist@^3.12.0", "@react-aria/gridlist@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@react-aria/gridlist/-/gridlist-3.14.2.tgz#0a4af99f895fbbba6d5882c1340ccb010c5c2a4f" - integrity sha512-c51ip0bc/lKppfrPNFHbWu1n/r0NHd9Xl114904cDxuRcElJ3H/V/3e3U9HyDy+4xioiXZIdZ75CNxtEoTmrxw== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/grid" "^3.14.6" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/list" "^3.13.2" - "@react-stately/tree" "^3.9.4" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/i18n@3.12.8": - version "3.12.8" - resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.12.8.tgz#43d534f04d3bfdef674ba94527cf7532875d8fc8" - integrity sha512-V/Nau9WuwTwxfFffQL4URyKyY2HhRlu9zmzkF2Hw/j5KmEQemD+9jfaLueG2CJu85lYL06JrZXUdnhZgKnqMkA== - dependencies: - "@internationalized/date" "^3.8.0" - "@internationalized/message" "^3.1.7" - "@internationalized/number" "^3.6.1" - "@internationalized/string" "^3.2.6" - "@react-aria/ssr" "^3.9.8" - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-aria/i18n@^3.12.14", "@react-aria/i18n@^3.12.8": - version "3.12.14" - resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.12.14.tgz#3c6d797dafc5d1d7b5c5dcda99883bee7e6fa81c" - integrity sha512-zYvs1FlLamFD49uneX3i5mPHrAsB3OjVpSWApTcPw8ydxOaphQDp/Q1aqrbcxlrQCcxZdXWHuvLlbkNR4+8jzw== - dependencies: - "@internationalized/date" "^3.10.1" - "@internationalized/message" "^3.1.8" - "@internationalized/number" "^3.6.5" - "@internationalized/string" "^3.2.7" - "@react-aria/ssr" "^3.9.10" - "@react-aria/utils" "^3.32.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/interactions@^3.25.0", "@react-aria/interactions@^3.26.0": - version "3.26.0" - resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.26.0.tgz#5c0538499fe2a8e02f15e37787ffba27996076b4" - integrity sha512-AAEcHiltjfbmP1i9iaVw34Mb7kbkiHpYdqieWufldh4aplWgsF11YQZOfaCJW4QoR2ML4Zzoa9nfFwLXA52R7Q== - dependencies: - "@react-aria/ssr" "^3.9.10" - "@react-aria/utils" "^3.32.0" - "@react-stately/flags" "^3.1.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/label@^3.7.17", "@react-aria/label@^3.7.23": - version "3.7.23" - resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.7.23.tgz#e8494b95b8f29806d5d1656419f912717eb127ef" - integrity sha512-dRkuCJfsyBHPTq3WOJVHNRvNyQL4cRRLELmjYfUX9/jQKIsUW2l71YnUHZTRCSn2ZjhdAcdwq96fNcQo0hncBQ== - dependencies: - "@react-aria/utils" "^3.32.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/landmark@^3.0.2", "@react-aria/landmark@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@react-aria/landmark/-/landmark-3.0.8.tgz#f19443510ccbf2bd826dd11365867ce91fcc1229" - integrity sha512-xuY8kYxCrF9C0h0Pj2lZHoxCidNfQ/SrkYWXuiN+LuBTJGCmPVif93gt7TklQ0rKJ+pKJsUgh8AC0pgwI3QP7A== - dependencies: - "@react-aria/utils" "^3.32.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - use-sync-external-store "^1.4.0" - -"@react-aria/link@^3.8.0", "@react-aria/link@^3.8.7": - version "3.8.7" - resolved "https://registry.yarnpkg.com/@react-aria/link/-/link-3.8.7.tgz#b6ae329c5477ad241f9cdaaf427107764f991d7e" - integrity sha512-TOC6Hf/x3N0P8SLR1KD/dGiJ9PmwAq8H57RiwbFbdINnG/HIvIQr5MxGTjwBvOOWcJu9brgWL5HkQaZK7Q/4Yw== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-types/link" "^3.6.5" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/listbox@^3.14.3", "@react-aria/listbox@^3.15.1": - version "3.15.1" - resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.15.1.tgz#f400b5928185960926cdf64880f98e99391cc9b7" - integrity sha512-81iDLFhmPXvLOtkI0SKzgrngfzwfR2o9oFDAYRfpYCOxgT7jjh8SaB4wCteJXRiMwymRGmgyTvD4yxWTluEeXA== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/collections" "^3.12.8" - "@react-stately/list" "^3.13.2" - "@react-types/listbox" "^3.7.4" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/live-announcer@^3.4.2", "@react-aria/live-announcer@^3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@react-aria/live-announcer/-/live-announcer-3.4.4.tgz#0e6533940222208b323b71d56ac8e115b2121e6a" - integrity sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA== - dependencies: - "@swc/helpers" "^0.5.0" - -"@react-aria/menu@^3.18.2", "@react-aria/menu@^3.19.4": - version "3.19.4" - resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.19.4.tgz#a2c641dfef4a07c7beecc8f693e534a8889de321" - integrity sha512-0A0DUEkEvZynmaD3zktHavM+EmgZSR/ht+g1ExS2jXe73CegA+dbSRfPl9eIKcHxaRrWOV96qMj2pTf0yWTBDg== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/overlays" "^3.31.0" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/collections" "^3.12.8" - "@react-stately/menu" "^3.9.9" - "@react-stately/selection" "^3.20.7" - "@react-stately/tree" "^3.9.4" - "@react-types/button" "^3.14.1" - "@react-types/menu" "^3.10.5" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/meter@^3.4.22", "@react-aria/meter@^3.4.28": - version "3.4.28" - resolved "https://registry.yarnpkg.com/@react-aria/meter/-/meter-3.4.28.tgz#5c84139acb8889709be71560574de91d4d8b779f" - integrity sha512-elACITUBOf4Dp+BQ2aIgHIe58fjWYjspxhVcE5BMiqePktOfRkpb9ESj8nWcNXO8eqCYwrFJpElHvXkjYLWemw== - dependencies: - "@react-aria/progress" "^3.4.28" - "@react-types/meter" "^3.4.13" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/numberfield@^3.11.13", "@react-aria/numberfield@^3.12.3": - version "3.12.3" - resolved "https://registry.yarnpkg.com/@react-aria/numberfield/-/numberfield-3.12.3.tgz#fc95e24c2c612baaf5ef9678ebe781cb2d197f5f" - integrity sha512-70LRXWPEuj2X8mbQXUx6l6We+RGs49Kb+2eUiSSLArHK4RvTWJWEfSjHL5IHHJ+j2AkbORdryD7SR3gcXSX+5w== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/spinbutton" "^3.7.0" - "@react-aria/textfield" "^3.18.3" - "@react-aria/utils" "^3.32.0" - "@react-stately/form" "^3.2.2" - "@react-stately/numberfield" "^3.10.3" - "@react-types/button" "^3.14.1" - "@react-types/numberfield" "^3.8.16" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/overlays@^3.27.0", "@react-aria/overlays@^3.31.0": - version "3.31.0" - resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.31.0.tgz#f62769e5aa63362b7389deb698b05fbea83c38e9" - integrity sha512-Vq41X1s8XheGIhGbbuqRJslJEX08qmMVX//dwuBaFX9T18mMR04tumKOMxp8Lz+vqwdGLvjNUYDMcgolL+AMjw== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/ssr" "^3.9.10" - "@react-aria/utils" "^3.32.0" - "@react-aria/visually-hidden" "^3.8.29" - "@react-stately/overlays" "^3.6.21" - "@react-types/button" "^3.14.1" - "@react-types/overlays" "^3.9.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/progress@^3.4.22", "@react-aria/progress@^3.4.28": - version "3.4.28" - resolved "https://registry.yarnpkg.com/@react-aria/progress/-/progress-3.4.28.tgz#97b5ffb049f9fee8f79365a69b1c7eb9e6f5b018" - integrity sha512-3NUUAu+rwf1M7pau9WFkrxe/PlBPiqCl/1maGU7iufVveHnz+SVVqXdNkjYx+WkPE0ViwG86Zx6OU4AYJ1pjNw== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/label" "^3.7.23" - "@react-aria/utils" "^3.32.0" - "@react-types/progress" "^3.5.16" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/radio@^3.11.2", "@react-aria/radio@^3.12.3": - version "3.12.3" - resolved "https://registry.yarnpkg.com/@react-aria/radio/-/radio-3.12.3.tgz#60a1acf005de09d421a30d8c7ac3b2aa9b96e80f" - integrity sha512-noucVX++9J3VYWg7dB+r09NVX8UZSR1TWUMCbT/MffzhltOsmiLJVvgJ0uEeeVRuu3+ZM63jOshrzG89anX4TQ== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/form" "^3.1.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/utils" "^3.32.0" - "@react-stately/radio" "^3.11.3" - "@react-types/radio" "^3.9.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/searchfield@^3.8.10", "@react-aria/searchfield@^3.8.3": - version "3.8.10" - resolved "https://registry.yarnpkg.com/@react-aria/searchfield/-/searchfield-3.8.10.tgz#ca5a155f50bca93594a69b58a1c6fb804d008dbf" - integrity sha512-1wMoSjXoekcETC4ZP5AUcWoaK96FssVuF9MgqQNqE5VnauQDjZBpPCfz6GSZwRHTGwoqb7CI4iEi7433kd50xg== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/textfield" "^3.18.3" - "@react-aria/utils" "^3.32.0" - "@react-stately/searchfield" "^3.5.17" - "@react-types/button" "^3.14.1" - "@react-types/searchfield" "^3.6.6" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/select@^3.15.4", "@react-aria/select@^3.17.1": - version "3.17.1" - resolved "https://registry.yarnpkg.com/@react-aria/select/-/select-3.17.1.tgz#78ba1bcad568a4c27de6ab09cecd2908136a2e9f" - integrity sha512-jPMuaSp+4SbdE9G5UrrTer2CPbbUnUSLd8I2wgRgGcyk3wFw9DtnUNfms+UBA/2SrVnAEJ6KCQAI0oiMK2m+tQ== - dependencies: - "@react-aria/form" "^3.1.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/listbox" "^3.15.1" - "@react-aria/menu" "^3.19.4" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-aria/visually-hidden" "^3.8.29" - "@react-stately/select" "^3.9.0" - "@react-types/button" "^3.14.1" - "@react-types/select" "^3.12.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/selection@^3.24.0", "@react-aria/selection@^3.27.0": - version "3.27.0" - resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.27.0.tgz#c7a4bcd81cd90e2fee3c47fac7322bd0a271df1d" - integrity sha512-4zgreuCu4QM4t2U7aF3mbMvIKCEkTEo6h6nGJvbyZALZ/eFtLTvUiV8/5CGDJRLGvgMvi3XxUeF9PZbpk5nMJg== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/selection" "^3.20.7" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/separator@^3.4.14", "@react-aria/separator@^3.4.8": - version "3.4.14" - resolved "https://registry.yarnpkg.com/@react-aria/separator/-/separator-3.4.14.tgz#183966fb63118ae1f42f5c3f1e9146ebbdc84022" - integrity sha512-a32OB5HMAmXEdExyDvsadsnlmNcVxxpx3tt+Jxxl6H9CHsLO+Ak077KGFJteGVg4bTfhWGAgczOsnvIioR88xw== - dependencies: - "@react-aria/utils" "^3.32.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/slider@^3.7.18", "@react-aria/slider@^3.8.3": - version "3.8.3" - resolved "https://registry.yarnpkg.com/@react-aria/slider/-/slider-3.8.3.tgz#09da65261ef491046a1256e6c9850b1c4bd5eb76" - integrity sha512-tOZVH+wLt3ik0C3wyuXqHL9fvnQ5S+/tHMYB7z8aZV5cEe36Gt4efBILphlA7ChkL/RvpHGK2AGpEGxvuEQIuQ== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/utils" "^3.32.0" - "@react-stately/slider" "^3.7.3" - "@react-types/shared" "^3.32.1" - "@react-types/slider" "^3.8.2" - "@swc/helpers" "^0.5.0" - -"@react-aria/spinbutton@^3.6.14", "@react-aria/spinbutton@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@react-aria/spinbutton/-/spinbutton-3.7.0.tgz#0ea85fca1241563d8df83f200ba1e42e071449ec" - integrity sha512-FOyH94BZp+jNhUJuZqXSubQZDNQEJyW/J19/gwCxQvQvxAP79dhDFshh1UtrL4EjbjIflmaOes+sH/XEHUnJVA== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/live-announcer" "^3.4.4" - "@react-aria/utils" "^3.32.0" - "@react-types/button" "^3.14.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/ssr@^3.9.10", "@react-aria/ssr@^3.9.8": - version "3.9.10" - resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.10.tgz#7fdc09e811944ce0df1d7e713de1449abd7435e6" - integrity sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ== - dependencies: - "@swc/helpers" "^0.5.0" - -"@react-aria/switch@^3.7.2", "@react-aria/switch@^3.7.9": - version "3.7.9" - resolved "https://registry.yarnpkg.com/@react-aria/switch/-/switch-3.7.9.tgz#4a940abb36a47ddafc24711286f70b5c11cd27df" - integrity sha512-RZtuFRXews0PBx8Fc2R/kqaIARD5YIM5uYtmwnWfY7y5bEsBGONxp0d+m2vDyY7yk+VNpVFBdwewY9GbZmH1CA== - dependencies: - "@react-aria/toggle" "^3.12.3" - "@react-stately/toggle" "^3.9.3" - "@react-types/shared" "^3.32.1" - "@react-types/switch" "^3.5.15" - "@swc/helpers" "^0.5.0" - -"@react-aria/table@^3.17.2", "@react-aria/table@^3.17.9": - version "3.17.9" - resolved "https://registry.yarnpkg.com/@react-aria/table/-/table-3.17.9.tgz#067587bccb3c3b9588adb9f4c05b089c605ce946" - integrity sha512-Jby561E1YfzoRgtp+RQuhDz4vnxlcqol9RTgQQ7FWXC2IcN9Pny1COU34LkA1cL9VeB9LJ0+qfMhGw4aAwaUmw== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/grid" "^3.14.6" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/live-announcer" "^3.4.4" - "@react-aria/utils" "^3.32.0" - "@react-aria/visually-hidden" "^3.8.29" - "@react-stately/collections" "^3.12.8" - "@react-stately/flags" "^3.1.2" - "@react-stately/table" "^3.15.2" - "@react-types/checkbox" "^3.10.2" - "@react-types/grid" "^3.3.6" - "@react-types/shared" "^3.32.1" - "@react-types/table" "^3.13.4" - "@swc/helpers" "^0.5.0" - -"@react-aria/tabs@^3.10.2", "@react-aria/tabs@^3.10.9": - version "3.10.9" - resolved "https://registry.yarnpkg.com/@react-aria/tabs/-/tabs-3.10.9.tgz#81bdac133fe3bcf42091934217eaaa7ce99731d2" - integrity sha512-2+FNd7Ohr3hrEgYrKdZW0FWbgybzTVZft6tw95oQ2+9PnjdDVdtzHliI+8HY8jzb4hTf4bU7O8n+s/HBlCBSIw== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/tabs" "^3.8.7" - "@react-types/shared" "^3.32.1" - "@react-types/tabs" "^3.3.20" - "@swc/helpers" "^0.5.0" - -"@react-aria/tag@^3.5.2", "@react-aria/tag@^3.7.3": - version "3.7.3" - resolved "https://registry.yarnpkg.com/@react-aria/tag/-/tag-3.7.3.tgz#b2309468913a2809a9b429a81772730e17b2e277" - integrity sha512-fonqGFxhpnlIDOz3u38y4+MG5wyAef9+oDybsCKaJ57K+D4BTvSmpGBemN/mcaxdabnYfyhasCm0H91Q9XRcCA== - dependencies: - "@react-aria/gridlist" "^3.14.2" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/list" "^3.13.2" - "@react-types/button" "^3.14.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/textfield@^3.17.2", "@react-aria/textfield@^3.18.3": - version "3.18.3" - resolved "https://registry.yarnpkg.com/@react-aria/textfield/-/textfield-3.18.3.tgz#c62639f83a88863b3de52d21d18726f856cc550d" - integrity sha512-ehiSHOKuKCwPdxFe7wGE0QJlSeeJR4iJuH+OdsYVlZzYbl9J/uAdGbpsj/zPhNtBo1g/Td76U8TtTlYRZ8lUZw== - dependencies: - "@react-aria/form" "^3.1.3" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/utils" "^3.32.0" - "@react-stately/form" "^3.2.2" - "@react-stately/utils" "^3.11.0" - "@react-types/shared" "^3.32.1" - "@react-types/textfield" "^3.12.6" - "@swc/helpers" "^0.5.0" - -"@react-aria/toast@^3.0.2", "@react-aria/toast@^3.0.9": - version "3.0.9" - resolved "https://registry.yarnpkg.com/@react-aria/toast/-/toast-3.0.9.tgz#feb1516089b2f7a4ef4e05d4a307f3fb1e2ea021" - integrity sha512-2sRitczXl5VEwyq97o8TVvq3bIqLA7EfA7dhDPkYlHGa4T1vzKkhNqgkskKd9+Tw7gqeFRFjnokh+es9jkM11g== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/landmark" "^3.0.8" - "@react-aria/utils" "^3.32.0" - "@react-stately/toast" "^3.1.2" - "@react-types/button" "^3.14.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/toggle@^3.12.3": - version "3.12.3" - resolved "https://registry.yarnpkg.com/@react-aria/toggle/-/toggle-3.12.3.tgz#64aecb3350d171e3ece1568068b60992c84a92d5" - integrity sha512-mciUbeVP99fRObnH5qLFrkKXX+5VKeV6BhFJlmz1eo3ltR/0xZKnUcycA2CGzmqtB70w09CAhr8NMEnpNH8dwQ== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/toggle" "^3.9.3" - "@react-types/checkbox" "^3.10.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/toolbar@3.0.0-beta.15": - version "3.0.0-beta.15" - resolved "https://registry.yarnpkg.com/@react-aria/toolbar/-/toolbar-3.0.0-beta.15.tgz#2b85e9a1f3e9185447e7164736cab7a859ed5f25" - integrity sha512-PNGpNIKIsCW8rxI9XXSADlLrSpikILJKKECyTRw9KwvXDRc44pezvdjGHCNinQcKsQoy5BtkK5cTSAyVqzzTXQ== - dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-aria/toolbar@3.0.0-beta.22": - version "3.0.0-beta.22" - resolved "https://registry.yarnpkg.com/@react-aria/toolbar/-/toolbar-3.0.0-beta.22.tgz#957b5eb666b0a2b4d2301b77c309115c59af5593" - integrity sha512-Q1gOj6N4vzvpGrIoNAxpUudEQP82UgQACENH/bcH8FnEMbSP7DHvVfDhj7GTU6ldMXO2cjqLhiidoUK53gkCiA== - dependencies: - "@react-aria/focus" "^3.21.3" - "@react-aria/i18n" "^3.12.14" - "@react-aria/utils" "^3.32.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/tooltip@^3.8.2", "@react-aria/tooltip@^3.9.0": - version "3.9.0" - resolved "https://registry.yarnpkg.com/@react-aria/tooltip/-/tooltip-3.9.0.tgz#94437aae1962190aa86a2e414b4be9d2951cca82" - integrity sha512-2O1DXEV8/+DeUq9dIlAfaNa7lSG+7FCZDuF+sNiPYnZM6tgFOrsId26uMF5EuwpVfOvXSSGnq0+6Ma2On7mZPg== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/tooltip" "^3.5.9" - "@react-types/shared" "^3.32.1" - "@react-types/tooltip" "^3.5.0" - "@swc/helpers" "^0.5.0" - -"@react-aria/tree@^3.0.2", "@react-aria/tree@^3.1.5": - version "3.1.5" - resolved "https://registry.yarnpkg.com/@react-aria/tree/-/tree-3.1.5.tgz#515d58ce3e12cc6757ceb01be55a4be26967c6f6" - integrity sha512-FAq7pAhRVrWU0U/8QbQIJfBqHuoCD+F9rR9ruoM3oL0vVIZxVN57ak/dhyge3EGlraTl9vzFi6IRceXiMuk5kg== - dependencies: - "@react-aria/gridlist" "^3.14.2" - "@react-aria/i18n" "^3.12.14" - "@react-aria/selection" "^3.27.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/tree" "^3.9.4" - "@react-types/button" "^3.14.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/utils@^3.28.2", "@react-aria/utils@^3.32.0": - version "3.32.0" - resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.32.0.tgz#ebc2d715dd3814145f0e7c722797fdb20e308e7f" - integrity sha512-/7Rud06+HVBIlTwmwmJa2W8xVtgxgzm0+kLbuFooZRzKDON6hhozS1dOMR/YLMxyJOaYOTpImcP4vRR9gL1hEg== - dependencies: - "@react-aria/ssr" "^3.9.10" - "@react-stately/flags" "^3.1.2" - "@react-stately/utils" "^3.11.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - clsx "^2.0.0" - -"@react-aria/virtualizer@^4.1.4": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@react-aria/virtualizer/-/virtualizer-4.1.11.tgz#5c0c3de213b3ba6a7f1282028ffb072f40d5d5f5" - integrity sha512-eYL//bX11Aox4Eh1BSZFX4I/4EdyVVWLjmpW+Y5qy4WajNrowjiuJJM7Fp1rQBlOAVuz0KbaDmFhiU3Z3rWjsw== - dependencies: - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-stately/virtualizer" "^4.4.4" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-aria/visually-hidden@^3.8.22", "@react-aria/visually-hidden@^3.8.29": - version "3.8.29" - resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.8.29.tgz#9acc4b171b914c3f0c20f92215e2be01a1873526" - integrity sha512-1joCP+MHBLd+YA6Gb08nMFfDBhOF0Kh1gR1SA8zoxEB5RMfQEEkufIB8k0GGwvHGSCK3gFyO8UAVsD0+rRYEyg== - dependencies: - "@react-aria/interactions" "^3.26.0" - "@react-aria/utils" "^3.32.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-dnd/asap@^4.0.0": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab" - integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg== - -"@react-dnd/invariant@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e" - integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw== - -"@react-dnd/shallowequal@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" - integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== - -"@react-stately/autocomplete@3.0.0-beta.1": - version "3.0.0-beta.1" - resolved "https://registry.yarnpkg.com/@react-stately/autocomplete/-/autocomplete-3.0.0-beta.1.tgz#2e6ab2ea9702523bf6e9a8fb95ec4b79cb1993bc" - integrity sha512-ohs6QOtJouQ+Y1+zRKiCzv57QogSTRuOA1QfrnIS1YPwKO1EDQXSqFkq2htK5+bN9GCm94yo6r4iX++SZKmLXA== - dependencies: - "@react-stately/utils" "^3.10.6" - "@swc/helpers" "^0.5.0" - -"@react-stately/calendar@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.8.0.tgz#c8b051ef97d940eb4c1b4ac140a48b71868b628f" - integrity sha512-YAuJiR9EtVThX91gU2ay/6YgPe0LvZWEssu4BS0Atnwk5cAo32gvF5FMta9ztH1LIULdZFaypU/C1mvnayMf+Q== - dependencies: - "@internationalized/date" "^3.8.0" - "@react-stately/utils" "^3.10.6" - "@react-types/calendar" "^3.7.0" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/calendar@^3.8.0", "@react-stately/calendar@^3.9.1": - version "3.9.1" - resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.9.1.tgz#ba294498bf96bba079308a70db9636efaab08180" - integrity sha512-q0Q8fivpQa1rcLg5daUVxwVj1smCp1VnpX9A5Q5PkI9lH9x+xdS0Y6eOqb8Ih3TKBDkx9/oEZonOX7RYNIzSig== - dependencies: - "@internationalized/date" "^3.10.1" - "@react-stately/utils" "^3.11.0" - "@react-types/calendar" "^3.8.1" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/checkbox@^3.6.13", "@react-stately/checkbox@^3.7.3": - version "3.7.3" - resolved "https://registry.yarnpkg.com/@react-stately/checkbox/-/checkbox-3.7.3.tgz#f47c05277419c72d33a590010fa5b89672c223a0" - integrity sha512-ve2K+uWT+NRM1JMn+tkWJDP2iBAaWvbZ0TbSXs371IUcTWaNW61HygZ+UFOB/frAZGloazEKGqAsX5XjFpgB9w== - dependencies: - "@react-stately/form" "^3.2.2" - "@react-stately/utils" "^3.11.0" - "@react-types/checkbox" "^3.10.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/collections@^3.12.3", "@react-stately/collections@^3.12.8": - version "3.12.8" - resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.12.8.tgz#f38692fb9c6384fb91d1c50052a40e595d3efa2c" - integrity sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ== - dependencies: - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/color@^3.8.4", "@react-stately/color@^3.9.3": - version "3.9.3" - resolved "https://registry.yarnpkg.com/@react-stately/color/-/color-3.9.3.tgz#94aa7de0d3eb435ed78177266e3d9c2d430f446f" - integrity sha512-H5lQgl07upsI7+cxTwYo639ziDDG1DFgOtq5pmC4Nxi8uNl8sR/8YeLaYuxyJiVkj2VLHBYRQ3+JcxrdduFvPQ== - dependencies: - "@internationalized/number" "^3.6.5" - "@internationalized/string" "^3.2.7" - "@react-stately/form" "^3.2.2" - "@react-stately/numberfield" "^3.10.3" - "@react-stately/slider" "^3.7.3" - "@react-stately/utils" "^3.11.0" - "@react-types/color" "^3.1.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/combobox@^3.10.4", "@react-stately/combobox@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@react-stately/combobox/-/combobox-3.12.1.tgz#b2c15417631f0fbbdc603811c0ab3bf45b93030a" - integrity sha512-RwfTTYgKJ9raIY+7grZ5DbfVRSO5pDjo/ur2VN/28LZzM0eOQrLFQ00vpBmY7/R64sHRpcXLDxpz5cqpKCdvTw== - dependencies: - "@react-stately/collections" "^3.12.8" - "@react-stately/form" "^3.2.2" - "@react-stately/list" "^3.13.2" - "@react-stately/overlays" "^3.6.21" - "@react-stately/utils" "^3.11.0" - "@react-types/combobox" "^3.13.10" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/data@^3.12.3", "@react-stately/data@^3.15.0": - version "3.15.0" - resolved "https://registry.yarnpkg.com/@react-stately/data/-/data-3.15.0.tgz#c4e4b020fe3d4e213f8450f455b0bce0eb9f52fa" - integrity sha512-ocP39NQQkrbtHVCPsqltNncpEHaONyYX/8s2UK9xeLRc+55NtDI2RZDKTUf/mi6H2SHxzEwLMQH8hWtEwC55mQ== - dependencies: - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/datepicker@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@react-stately/datepicker/-/datepicker-3.14.0.tgz#5f7e282d56e038ac0ef1b620fb9c1ff86253af57" - integrity sha512-JSkQfKW0+WpPQyOOeRPBLwXkVfpTUwgZJDnHBCud5kEuQiFFyeAIbL57RNXc4AX2pzY3piQa6OHnjDGTfqClxQ== - dependencies: - "@internationalized/date" "^3.8.0" - "@internationalized/string" "^3.2.6" - "@react-stately/form" "^3.1.3" - "@react-stately/overlays" "^3.6.15" - "@react-stately/utils" "^3.10.6" - "@react-types/datepicker" "^3.12.0" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/datepicker@^3.14.0", "@react-stately/datepicker@^3.15.3": - version "3.15.3" - resolved "https://registry.yarnpkg.com/@react-stately/datepicker/-/datepicker-3.15.3.tgz#b04d5799664f58cbcae6fe116f982881d9cfd4b6" - integrity sha512-RDYoz1R/EkCyxHYewb58T7DngU3gl6CnQL7xiWiDlayPnstGaanoQ3yCZGJaIQwR8PrKdNbQwXF9NlSmj8iCOw== - dependencies: - "@internationalized/date" "^3.10.1" - "@internationalized/string" "^3.2.7" - "@react-stately/form" "^3.2.2" - "@react-stately/overlays" "^3.6.21" - "@react-stately/utils" "^3.11.0" - "@react-types/datepicker" "^3.13.3" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/disclosure@^3.0.3", "@react-stately/disclosure@^3.0.9": - version "3.0.9" - resolved "https://registry.yarnpkg.com/@react-stately/disclosure/-/disclosure-3.0.9.tgz#3f2a4d95243e04c567f1495e737b685c24e46c51" - integrity sha512-M3HKsXqdzYKQf1TpnQRLZ6+/b8E3Nba3oOuY0OW5NnM5dZWSnXuj8foBQJT118FdLgMjpfBdPIkUvnaGiDCs5w== - dependencies: - "@react-stately/utils" "^3.11.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/dnd@^3.5.3", "@react-stately/dnd@^3.7.2": - version "3.7.2" - resolved "https://registry.yarnpkg.com/@react-stately/dnd/-/dnd-3.7.2.tgz#fd3c97003a00f42732e729476199e63c4b9faeb7" - integrity sha512-tr5nNgrLMn5GV308K1f010XUZ2j8CApqHrrcjg5fa2AnpO2gECcOf+UEnAvoFNUsvknje4iPX8y0/0No2ZHsgA== - dependencies: - "@react-stately/selection" "^3.20.7" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/flags@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-stately/flags/-/flags-3.1.2.tgz#5c8e5ae416d37d37e2e583d2fcb3a046293504f2" - integrity sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg== - dependencies: - "@swc/helpers" "^0.5.0" - -"@react-stately/form@^3.1.3", "@react-stately/form@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@react-stately/form/-/form-3.2.2.tgz#c1ae1264a414dd5e3e8ae0f21baf4814588d8c53" - integrity sha512-soAheOd7oaTO6eNs6LXnfn0tTqvOoe3zN9FvtIhhrErKz9XPc5sUmh3QWwR45+zKbitOi1HOjfA/gifKhZcfWw== - dependencies: - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/grid@^3.11.7": - version "3.11.7" - resolved "https://registry.yarnpkg.com/@react-stately/grid/-/grid-3.11.7.tgz#7ca4277f354a52940b905206b7665d55c2dd12c7" - integrity sha512-SqzBSxUTFZKLZicfXDK+M0A3gh07AYK1pmU/otcq2cjZ0nSC4CceKijQ2GBZnl+YGcGHI1RgkhpLP6ZioMYctQ== - dependencies: - "@react-stately/collections" "^3.12.8" - "@react-stately/selection" "^3.20.7" - "@react-types/grid" "^3.3.6" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/layout@^4.2.2": - version "4.5.2" - resolved "https://registry.yarnpkg.com/@react-stately/layout/-/layout-4.5.2.tgz#dbffe2a00e02c29274cd2b650eb37c3441d1c1b3" - integrity sha512-quAzYkshApkv1vChz2NXBaLTC7ihJUmv3ijqJBHCkZSY6qq+1qnc4aGespDF1f3mPhmpGswTFGXFImFTAYfi5g== - dependencies: - "@react-stately/collections" "^3.12.8" - "@react-stately/table" "^3.15.2" - "@react-stately/virtualizer" "^4.4.4" - "@react-types/grid" "^3.3.6" - "@react-types/shared" "^3.32.1" - "@react-types/table" "^3.13.4" - "@swc/helpers" "^0.5.0" - -"@react-stately/list@^3.12.1", "@react-stately/list@^3.13.2": - version "3.13.2" - resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.13.2.tgz#e4442cf6df439c199464c6fae4633ee5a6648bb7" - integrity sha512-dGFALuQWNNOkv7W12qSsXLF4mJHLeWeK2hVvdyj4SI8Vxku+BOfaVKuW3sn3mNiixI1dM/7FY2ip4kK+kv27vw== - dependencies: - "@react-stately/collections" "^3.12.8" - "@react-stately/selection" "^3.20.7" - "@react-stately/utils" "^3.11.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/menu@^3.9.3", "@react-stately/menu@^3.9.9": - version "3.9.9" - resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.9.9.tgz#9b5489759293e8f4c12a4f86ca55a85fbd24935a" - integrity sha512-moW5JANxMxPilfR0SygpCWCZe7Ef09oadgzTZthRymNRv0PXVS9ad4wd1EkwuMvPH/n0uZLZE2s8hNyFDgyqPA== - dependencies: - "@react-stately/overlays" "^3.6.21" - "@react-types/menu" "^3.10.5" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/numberfield@^3.10.3", "@react-stately/numberfield@^3.9.11": - version "3.10.3" - resolved "https://registry.yarnpkg.com/@react-stately/numberfield/-/numberfield-3.10.3.tgz#d1e211c12a975c61591025522db2454c4f3ec1e7" - integrity sha512-40g/oyVcWoEaLqkr61KuHZzQVLLXFi3oa2K8XLnb6o+859SM4TX3XPNqL6eNQjXSKoJO5Hlgpqhee9j+VDbGog== - dependencies: - "@internationalized/number" "^3.6.5" - "@react-stately/form" "^3.2.2" - "@react-stately/utils" "^3.11.0" - "@react-types/numberfield" "^3.8.16" - "@swc/helpers" "^0.5.0" - -"@react-stately/overlays@^3.6.15", "@react-stately/overlays@^3.6.21": - version "3.6.21" - resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.6.21.tgz#01e73a13e828da187acb9312c8b1a85474a46737" - integrity sha512-7f25H1PS2g+SNvuWPEW30pSGqYNHxesCP4w+1RcV/XV1oQI7oP5Ji2WfI0QsJEFc9wP/ZO1pyjHNKpfLI3O88g== - dependencies: - "@react-stately/utils" "^3.11.0" - "@react-types/overlays" "^3.9.2" - "@swc/helpers" "^0.5.0" - -"@react-stately/radio@^3.10.12", "@react-stately/radio@^3.11.3": - version "3.11.3" - resolved "https://registry.yarnpkg.com/@react-stately/radio/-/radio-3.11.3.tgz#c25a86e4acc5a6904904ed3272ee18ceb9bc9912" - integrity sha512-8+Cy0azV1aBWKcBfGHi3nBa285lAS6XhmVw2LfEwxq8DeVKTbJAaCHHwvDoclxDiOAnqzE0pio0QMD8rYISt9g== - dependencies: - "@react-stately/form" "^3.2.2" - "@react-stately/utils" "^3.11.0" - "@react-types/radio" "^3.9.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/searchfield@^3.5.11", "@react-stately/searchfield@^3.5.17": - version "3.5.17" - resolved "https://registry.yarnpkg.com/@react-stately/searchfield/-/searchfield-3.5.17.tgz#cc331362318833a5ab2ab6a210a306ebeab2ad14" - integrity sha512-/KExpJt6EGyuLxy/PRQJlETQxJGw8tRxVws6qF1lankN49Os2UhFEWi7ogbMCOWN67gIgevhZRdzmJnuov6BEQ== - dependencies: - "@react-stately/utils" "^3.11.0" - "@react-types/searchfield" "^3.6.6" - "@swc/helpers" "^0.5.0" - -"@react-stately/select@^3.6.12", "@react-stately/select@^3.9.0": - version "3.9.0" - resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.9.0.tgz#ae744ecad62f6527bc58d06b2e6f73676cdd6ca2" - integrity sha512-eNE33zVYpVdCPKRPGYyViN3LnEq82e1wjBIrs9T7Vo4EBnJeT57pqMZpalTPk7qsA+861t14Qrj7GnUd+YbEXw== - dependencies: - "@react-stately/form" "^3.2.2" - "@react-stately/list" "^3.13.2" - "@react-stately/overlays" "^3.6.21" - "@react-stately/utils" "^3.11.0" - "@react-types/select" "^3.12.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/selection@^3.20.1", "@react-stately/selection@^3.20.7": - version "3.20.7" - resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.20.7.tgz#9b7fe1aab9f2d840eec994c4e52aa5c21d52faf1" - integrity sha512-NkiRsNCfORBIHNF1bCavh4Vvj+Yd5NffE10iXtaFuhF249NlxLynJZmkcVCqNP9taC2pBIHX00+9tcBgxhG+mA== - dependencies: - "@react-stately/collections" "^3.12.8" - "@react-stately/utils" "^3.11.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/slider@^3.6.3", "@react-stately/slider@^3.7.3": - version "3.7.3" - resolved "https://registry.yarnpkg.com/@react-stately/slider/-/slider-3.7.3.tgz#fa60cd71f73b65c93cb7dab1ab703d1dcfd6ddc0" - integrity sha512-9QGnQNXFAH52BzxtU7weyOV/VV7/so6uIvE8VOHfc6QR3GMBM/kJvqBCTWZfQ0pxDIsRagBQDD/tjB09ixTOzg== - dependencies: - "@react-stately/utils" "^3.11.0" - "@react-types/shared" "^3.32.1" - "@react-types/slider" "^3.8.2" - "@swc/helpers" "^0.5.0" - -"@react-stately/table@^3.14.1", "@react-stately/table@^3.15.2": - version "3.15.2" - resolved "https://registry.yarnpkg.com/@react-stately/table/-/table-3.15.2.tgz#9d5dff05e9ebfb7006a190c186977e293a3e8d7a" - integrity sha512-vgEArBN5ocqsQdeORBj6xk8acu5iFnd/CyXEQKl0R5RyuYuw0ms8UmFHvs8Fv1HONehPYg+XR4QPliDFPX8R9A== - dependencies: - "@react-stately/collections" "^3.12.8" - "@react-stately/flags" "^3.1.2" - "@react-stately/grid" "^3.11.7" - "@react-stately/selection" "^3.20.7" - "@react-stately/utils" "^3.11.0" - "@react-types/grid" "^3.3.6" - "@react-types/shared" "^3.32.1" - "@react-types/table" "^3.13.4" - "@swc/helpers" "^0.5.0" - -"@react-stately/tabs@^3.8.1", "@react-stately/tabs@^3.8.7": - version "3.8.7" - resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.8.7.tgz#e9a915601337cd631bb309382db20871a473e9fb" - integrity sha512-ETZEzg7s9F2SCvisZ2cCpLx6XBHqdvVgDGU5l3C3s9zBKBr6lgyLFt61IdGW8XXZRUvw4mMGT6tGQbXeGvR0Wg== - dependencies: - "@react-stately/list" "^3.13.2" - "@react-types/shared" "^3.32.1" - "@react-types/tabs" "^3.3.20" - "@swc/helpers" "^0.5.0" - -"@react-stately/toast@^3.1.0", "@react-stately/toast@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-stately/toast/-/toast-3.1.2.tgz#0502040b6bd57479eaba1bca2f4c66e9e957e55a" - integrity sha512-HiInm7bck32khFBHZThTQaAF6e6/qm57F4mYRWdTq8IVeGDzpkbUYibnLxRhk0UZ5ybc6me+nqqPkG/lVmM42Q== - dependencies: - "@swc/helpers" "^0.5.0" - use-sync-external-store "^1.4.0" - -"@react-stately/toggle@^3.8.3", "@react-stately/toggle@^3.9.3": - version "3.9.3" - resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.9.3.tgz#9a08562d72b5051f97f97b4b48d40333abac23e7" - integrity sha512-G6aA/aTnid/6dQ9dxNEd7/JqzRmVkVYYpOAP+l02hepiuSmFwLu4nE98i4YFBQqFZ5b4l01gMrS90JGL7HrNmw== - dependencies: - "@react-stately/utils" "^3.11.0" - "@react-types/checkbox" "^3.10.2" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/tooltip@^3.5.3", "@react-stately/tooltip@^3.5.9": - version "3.5.9" - resolved "https://registry.yarnpkg.com/@react-stately/tooltip/-/tooltip-3.5.9.tgz#72c0b25e01fc89d89592b64f8232bd895701c60a" - integrity sha512-YwqtxFqQFfJtbeh+axHVGAfz9XHf73UaBndHxSbVM/T5c1PfI2yOB39T2FOU5fskZ2VMO3qTDhiXmFgGbGYSfQ== - dependencies: - "@react-stately/overlays" "^3.6.21" - "@react-types/tooltip" "^3.5.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/tree@^3.8.9", "@react-stately/tree@^3.9.4": - version "3.9.4" - resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.9.4.tgz#8e636aaf0b330116405ad57e874c79f1eebfad49" - integrity sha512-Re1fdEiR0hHPcEda+7ecw+52lgGfFW0MAEDzFg9I6J/t8STQSP+1YC0VVVkv2xRrkLbKLPqggNKgmD8nggecnw== - dependencies: - "@react-stately/collections" "^3.12.8" - "@react-stately/selection" "^3.20.7" - "@react-stately/utils" "^3.11.0" - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-stately/utils@^3.10.6", "@react-stately/utils@^3.11.0": - version "3.11.0" - resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.11.0.tgz#95a05d9633f4614ca89f630622566e7e5709d79e" - integrity sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw== - dependencies: - "@swc/helpers" "^0.5.0" - -"@react-stately/virtualizer@^4.3.2", "@react-stately/virtualizer@^4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@react-stately/virtualizer/-/virtualizer-4.4.4.tgz#b2a48bf6bf22f59532d9c6d71d010845b0088ed1" - integrity sha512-ri8giqXSZOrznZDCCOE4U36wSkOhy+hrFK7yo/YVcpxTqqp3d3eisfKMqbDsgqBW+XTHycTU/xeAf0u9NqrfpQ== - dependencies: - "@react-types/shared" "^3.32.1" - "@swc/helpers" "^0.5.0" - -"@react-types/autocomplete@3.0.0-alpha.30": - version "3.0.0-alpha.30" - resolved "https://registry.yarnpkg.com/@react-types/autocomplete/-/autocomplete-3.0.0-alpha.30.tgz#c4672644f762d112557c54e78fddf1f8f9ae4146" - integrity sha512-9neGygI+stJqiEFHzoc1jMySj6lOc4MUmBmu0uGn2zdOG2zxaAZSjh1pd9AJkHNyZ4j/n5rVXMo+v3RNkUntNw== - dependencies: - "@react-types/combobox" "^3.13.4" - "@react-types/searchfield" "^3.6.1" - "@react-types/shared" "^3.29.0" - -"@react-types/breadcrumbs@^3.7.17": - version "3.7.17" - resolved "https://registry.yarnpkg.com/@react-types/breadcrumbs/-/breadcrumbs-3.7.17.tgz#e584352cc4f79e638e20774af04f28064cd9b66b" - integrity sha512-IhvVTcfli5o/UDlGACXxjlor2afGlMQA8pNR3faH0bBUay1Fmm3IWktVw9Xwmk+KraV2RTAg9e+E6p8DOQZfiw== - dependencies: - "@react-types/link" "^3.6.5" - "@react-types/shared" "^3.32.1" - -"@react-types/button@^3.12.0", "@react-types/button@^3.14.1": - version "3.14.1" - resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.14.1.tgz#41c25f7c7dd1b31a359a6af9cbec5bed5dbb5aa1" - integrity sha512-D8C4IEwKB7zEtiWYVJ3WE/5HDcWlze9mLWQ5hfsBfpePyWCgO3bT/+wjb/7pJvcAocrkXo90QrMm85LcpBtrpg== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/calendar@^3.7.0", "@react-types/calendar@^3.8.1": - version "3.8.1" - resolved "https://registry.yarnpkg.com/@react-types/calendar/-/calendar-3.8.1.tgz#799fd86756829852b415b45cb02bafc633a010d8" - integrity sha512-B0UuitMP7YkArBAQldwSZSNL2WwazNGCG+lp6yEDj831NrH9e36/jcjv1rObQ9ZMS6uDX9LXu5C8V5RFwGQabA== - dependencies: - "@internationalized/date" "^3.10.1" - "@react-types/shared" "^3.32.1" - -"@react-types/checkbox@^3.10.2": - version "3.10.2" - resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.10.2.tgz#0190c9690e3649348a4d7045b168cbda657ce9de" - integrity sha512-ktPkl6ZfIdGS1tIaGSU/2S5Agf2NvXI9qAgtdMDNva0oLyAZ4RLQb6WecPvofw1J7YKXu0VA5Mu7nlX+FM2weQ== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/color@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-types/color/-/color-3.1.2.tgz#06a0e81b463f3a8d4553e7cd535b2881d721e231" - integrity sha512-NP0TAY3j4tlMztOp/bBfMlPwC9AQKTjSiTFmc2oQNkx5M4sl3QpPqFPosdt7jZ8M4nItvfCWZrlZGjST4SB83A== - dependencies: - "@react-types/shared" "^3.32.1" - "@react-types/slider" "^3.8.2" - -"@react-types/combobox@^3.13.10", "@react-types/combobox@^3.13.4": - version "3.13.10" - resolved "https://registry.yarnpkg.com/@react-types/combobox/-/combobox-3.13.10.tgz#d9e0c547a825f0f194ab47516b5f11f19d7b02a1" - integrity sha512-Wo4iix++ID6JzoH9eD7ddGUlirQiGpN/VQc3iFjnaTXiJ/cj3v+1oGsDGCZZTklTVeUMU7SRBfMhMgxHHIYLXA== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/datepicker@^3.12.0", "@react-types/datepicker@^3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@react-types/datepicker/-/datepicker-3.13.3.tgz#96d80b330f942f6bf7f2594faaefcb99557bbc5e" - integrity sha512-OTRa3banGxcUQKRTLUzr0zTVUMUL+Az1BWARCYQ+8Z/dlkYXYUW0fnS5I0pUEqihgai15KxiY13U0gAqbNSfcA== - dependencies: - "@internationalized/date" "^3.10.1" - "@react-types/calendar" "^3.8.1" - "@react-types/overlays" "^3.9.2" - "@react-types/shared" "^3.32.1" - -"@react-types/dialog@^3.5.17", "@react-types/dialog@^3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.5.22.tgz#ed772c303042c6ee5e8a9bc9e58b5b9d08ed634b" - integrity sha512-smSvzOcqKE196rWk0oqJDnz+ox5JM5+OT0PmmJXiUD4q7P5g32O6W5Bg7hMIFUI9clBtngo8kLaX2iMg+GqAzg== - dependencies: - "@react-types/overlays" "^3.9.2" - "@react-types/shared" "^3.32.1" - -"@react-types/form@^3.7.11": - version "3.7.16" - resolved "https://registry.yarnpkg.com/@react-types/form/-/form-3.7.16.tgz#4285880a03db2b9f664246b6f2f2421ef18a49da" - integrity sha512-Sb7KJoWEaQ/e4XIY+xRbjKvbP1luome98ZXevpD+zVSyGjEcfIroebizP6K1yMHCWP/043xH6GUkgEqWPoVGjg== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/grid@^3.3.1", "@react-types/grid@^3.3.6": - version "3.3.6" - resolved "https://registry.yarnpkg.com/@react-types/grid/-/grid-3.3.6.tgz#9fc60f5baa8a1668bb2257a981dd514dd64706c3" - integrity sha512-vIZJlYTii2n1We9nAugXwM2wpcpsC6JigJFBd6vGhStRdRWRoU4yv1Gc98Usbx0FQ/J7GLVIgeG8+1VMTKBdxw== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/link@^3.6.5": - version "3.6.5" - resolved "https://registry.yarnpkg.com/@react-types/link/-/link-3.6.5.tgz#28cf2b90f69e83af7ff507021eec0449f148eb45" - integrity sha512-+I2s3XWBEvLrzts0GnNeA84mUkwo+a7kLUWoaJkW0TOBDG7my95HFYxF9WnqKye7NgpOkCqz4s3oW96xPdIniQ== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/listbox@^3.7.4": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.7.4.tgz#712329de62c0fddf44c062bb18c51135309c9fe8" - integrity sha512-p4YEpTl/VQGrqVE8GIfqTS5LkT5jtjDTbVeZgrkPnX/fiPhsfbTPiZ6g0FNap4+aOGJFGEEZUv2q4vx+rCORww== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/menu@^3.10.5": - version "3.10.5" - resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.10.5.tgz#0e3a66aab3c636dc3ef965529640bb678b13a431" - integrity sha512-HBTrKll2hm0VKJNM4ubIv1L9MNo8JuOnm2G3M+wXvb6EYIyDNxxJkhjsqsGpUXJdAOSkacHBDcNh2HsZABNX4A== - dependencies: - "@react-types/overlays" "^3.9.2" - "@react-types/shared" "^3.32.1" - -"@react-types/meter@^3.4.13": - version "3.4.13" - resolved "https://registry.yarnpkg.com/@react-types/meter/-/meter-3.4.13.tgz#ddd54e1d56a9af79646d832a3cf4a8979e473ead" - integrity sha512-EiarfbpHcvmeyXvXcr6XLaHkNHuGc4g7fBVEiDPwssFJKKfbUzqnnknDxPjyspqUVRcXC08CokS98J1jYobqDg== - dependencies: - "@react-types/progress" "^3.5.16" - -"@react-types/numberfield@^3.8.16": - version "3.8.16" - resolved "https://registry.yarnpkg.com/@react-types/numberfield/-/numberfield-3.8.16.tgz#97e3f02e16843250718e347eee16f30ee0720362" - integrity sha512-945F0GsD7K2T293YXhap+2Runl3tZWbnhadXVHFWLbqIKKONZFSZTfLKxQcbFr+bQXr2uh1bVJhYcOiS1l5M+A== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/overlays@^3.9.2": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.9.2.tgz#721dc248afcb42db988391037a6386bb61556cd4" - integrity sha512-Q0cRPcBGzNGmC8dBuHyoPR7N3057KTS5g+vZfQ53k8WwmilXBtemFJPLsogJbspuewQ/QJ3o2HYsp2pne7/iNw== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/progress@^3.5.16": - version "3.5.16" - resolved "https://registry.yarnpkg.com/@react-types/progress/-/progress-3.5.16.tgz#f40e1b674e630276d6412297a92eb0dd3761d426" - integrity sha512-I9tSdCFfvQ7gHJtm90VAKgwdTWXQgVNvLRStEc0z9h+bXBxdvZb+QuiRPERChwFQ9VkK4p4rDqaFo69nDqWkpw== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/radio@^3.9.2": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@react-types/radio/-/radio-3.9.2.tgz#c6ac396b49cc03334e4bb63a96f366effce3a615" - integrity sha512-3UcJXu37JrTkRyP4GJPDBU7NmDTInrEdOe+bVzA1j4EegzdkJmLBkLg5cLDAbpiEHB+xIsvbJdx6dxeMuc+H3g== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/searchfield@^3.6.1", "@react-types/searchfield@^3.6.6": - version "3.6.6" - resolved "https://registry.yarnpkg.com/@react-types/searchfield/-/searchfield-3.6.6.tgz#fd41bd78246789ac4284462abe222b6f85d191b2" - integrity sha512-cl3itr/fk7wbIQc2Gz5Ie8aVeUmPjVX/mRGS5/EXlmzycAKNYTvqf2mlxwObLndtLISmt7IgNjRRhbUUDI8Ang== - dependencies: - "@react-types/shared" "^3.32.1" - "@react-types/textfield" "^3.12.6" - -"@react-types/select@^3.12.0": - version "3.12.0" - resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.12.0.tgz#40cbc3a319a003cba4aacb9040cbd6ab48485601" - integrity sha512-tM3mEbQNotvCJs1gYRFyIeXmXrIBSBLGw7feCIaYSO45IyjCGv8NZwpQWjoKPaWo3GpbHfHMNlWlq3v5QQPIXw== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/shared@^3.29.0", "@react-types/shared@^3.32.1": - version "3.32.1" - resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.32.1.tgz#abfeb839d65d0abe923576f34ac08342c25dfa55" - integrity sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w== - -"@react-types/slider@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@react-types/slider/-/slider-3.8.2.tgz#37e02c42514aca842954c715082d1ae06a661ee4" - integrity sha512-MQYZP76OEOYe7/yA2To+Dl0LNb0cKKnvh5JtvNvDnAvEprn1RuLiay8Oi/rTtXmc2KmBa4VdTcsXsmkbbkeN2Q== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/switch@^3.5.15": - version "3.5.15" - resolved "https://registry.yarnpkg.com/@react-types/switch/-/switch-3.5.15.tgz#43806d6e7034d1d07e20c89ec9618e607fef9e62" - integrity sha512-r/ouGWQmIeHyYSP1e5luET+oiR7N7cLrAlWsrAfYRWHxqXOSNQloQnZJ3PLHrKFT02fsrQhx2rHaK2LfKeyN3A== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/table@^3.12.0", "@react-types/table@^3.13.4": - version "3.13.4" - resolved "https://registry.yarnpkg.com/@react-types/table/-/table-3.13.4.tgz#c3d5952d4bd2a04992255850538856829b12c99a" - integrity sha512-I/DYiZQl6aNbMmjk90J9SOhkzVDZvyA3Vn3wMWCiajkMNjvubFhTfda5DDf2SgFP5l0Yh6TGGH5XumRv9LqL5Q== - dependencies: - "@react-types/grid" "^3.3.6" - "@react-types/shared" "^3.32.1" - -"@react-types/tabs@^3.3.20": - version "3.3.20" - resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.3.20.tgz#57c1563a6214298ce0ede8f160a625d92e3b0bcd" - integrity sha512-Kjq4PypapdMOVPAQgaFIKH65Kr3YnRvaxBGd6RYizTsqYImQhXoGj6B4lBpjYy4KhfRd4dYS82frHqTGKmBYiA== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/textfield@^3.12.6": - version "3.12.6" - resolved "https://registry.yarnpkg.com/@react-types/textfield/-/textfield-3.12.6.tgz#0f90a4108c8d75e7c2c26753e760cd580339be12" - integrity sha512-hpEVKE+M3uUkTjw2WrX1NrH/B3rqDJFUa+ViNK2eVranLY4ZwFqbqaYXSzHupOF3ecSjJJv2C103JrwFvx6TPQ== - dependencies: - "@react-types/shared" "^3.32.1" - -"@react-types/tooltip@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@react-types/tooltip/-/tooltip-3.5.0.tgz#959a0a798f2c1625341051942692218c6564877f" - integrity sha512-o/m1wlKlOD2sLb9vZLWdVkD5LFLHBMLGeeK/bhyUtp0IEdUeKy0ZRTS7pa/A50trov9RvdbzLK79xG8nKNxHew== - dependencies: - "@react-types/overlays" "^3.9.2" - "@react-types/shared" "^3.32.1" - -"@rtsao/scc@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" - integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== - -"@rushstack/eslint-patch@^1.10.3": - version "1.15.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz#8184bcb37791e6d3c3c13a9bfbe4af263f66665f" - integrity sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw== - -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - -"@sinonjs/commons@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" - integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== - dependencies: - type-detect "4.0.8" - -"@sinonjs/fake-timers@^10.0.2": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" - integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== - dependencies: - "@sinonjs/commons" "^3.0.0" - -"@standard-schema/spec@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" - integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== - -"@sveltejs/acorn-typescript@^1.0.5": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz#69c746a7c232094c117c50dedbd1279fc64887b7" - integrity sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA== - -"@swc/helpers@0.5.15": - version "0.5.15" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" - integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== - dependencies: - tslib "^2.8.0" - -"@swc/helpers@^0.5.0": - version "0.5.18" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.18.tgz#feeeabea0d10106ee25aaf900165df911ab6d3b1" - integrity sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ== - dependencies: - tslib "^2.8.0" - -"@tanstack/eslint-plugin-query@5.66.1": - version "5.66.1" - resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.66.1.tgz#9951e4d3633ca6248196742cad437b4d7c760cd7" - integrity sha512-pYMVTGgJ7yPk9Rm6UWEmbY6TX0EmMmxJqYkthgeDCwEznToy2m+W928nUODFirtZBZlhBsqHy33LO0kyTlgf0w== - dependencies: - "@typescript-eslint/utils" "^8.18.1" - -"@tanstack/query-core@5.90.10": - version "5.90.10" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.10.tgz#2704b038277fb41e631f356e7428951b5354ed13" - integrity sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ== - -"@tanstack/query-devtools@5.65.0": - version "5.65.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz#37da5e911543b4f6d98b9a04369eab0de6044ba1" - integrity sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg== - -"@tanstack/react-query-devtools@5.66.9": - version "5.66.9" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.66.9.tgz#5cb69b874682289e5014bcf9b6460a2d35df129f" - integrity sha512-70G6AR35he53SYUcUK6EdqNR18zejCv1rM6900gjZP408EAex56YLwVSeijzk9lWeU2J42G9Fjh0i1WngUTsgw== - dependencies: - "@tanstack/query-devtools" "5.65.0" - -"@tanstack/react-query@5.90.10": - version "5.90.10" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.10.tgz#dc2d4acbe1c06b708ccaeed43a26ad56bc5d1718" - integrity sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw== - dependencies: - "@tanstack/query-core" "5.90.10" - -"@tanstack/react-table@8.21.3": - version "8.21.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b" - integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== - dependencies: - "@tanstack/table-core" "8.21.3" - -"@tanstack/table-core@8.21.3": - version "8.21.3" - resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" - integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== - -"@tsconfig/node10@^1.0.7": - version "1.0.12" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" - integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - -"@tybys/wasm-util@^0.10.0": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" - integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== - dependencies: - tslib "^2.4.0" - -"@types/babel__core@^7.1.14": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" - integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" - integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" - integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== - dependencies: - "@babel/types" "^7.28.2" - -"@types/estree@^1.0.5", "@types/estree@^1.0.6": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - -"@types/graceful-fs@^4.1.3": - version "4.1.9" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" - integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== - dependencies: - "@types/node" "*" - -"@types/hoist-non-react-statics@^3.3.6": - version "3.3.7" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c" - integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== - dependencies: - hoist-non-react-statics "^3.3.0" - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" - integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== - -"@types/istanbul-lib-report@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" - integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" - integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/jest@29.5.14": - version "29.5.14" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" - integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== - dependencies: - expect "^29.0.0" - pretty-format "^29.0.0" - -"@types/json-schema@^7.0.15": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - -"@types/minimatch@3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" - integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== - -"@types/minimatch@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-6.0.0.tgz#4d207b1cc941367bdcd195a3a781a7e4fc3b1e03" - integrity sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA== - dependencies: - minimatch "*" - -"@types/node@*": - version "25.0.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7" - integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg== - dependencies: - undici-types "~7.16.0" - -"@types/node@22.10.7": - version "22.10.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.7.tgz#14a1ca33fd0ebdd9d63593ed8d3fbc882a6d28d7" - integrity sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg== - dependencies: - undici-types "~6.20.0" - -"@types/node@24.10.1": - version "24.10.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01" - integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ== - dependencies: - undici-types "~7.16.0" - -"@types/react-dom@19.2.3": - version "19.2.3" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" - integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== - -"@types/react-modal@3.16.3": - version "3.16.3" - resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.16.3.tgz#250f32c07f1de28e2bcf9c3e84b56adaa6897013" - integrity sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg== - dependencies: - "@types/react" "*" - -"@types/react@*": - version "19.2.9" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.9.tgz#84ec7669742bb3e7e2e8d6a5258d95ead7764200" - integrity sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA== - dependencies: - csstype "^3.2.2" - -"@types/react@19.2.5": - version "19.2.5" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.5.tgz#bb75da5c52a956ba7d2623dffbba0fdadc3e4145" - integrity sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw== - dependencies: - csstype "^3.0.2" - -"@types/stack-utils@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" - integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== - -"@types/yargs-parser@*": - version "21.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" - integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== - -"@types/yargs@^17.0.8": - version "17.0.35" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" - integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== - dependencies: - "@types/yargs-parser" "*" - -"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz#f6640f6f8749b71d9ab457263939e8932a3c6b46" - integrity sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag== - dependencies: - "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.53.1" - "@typescript-eslint/type-utils" "8.53.1" - "@typescript-eslint/utils" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" - ignore "^7.0.5" - natural-compare "^1.4.0" - ts-api-utils "^2.4.0" - -"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.53.1.tgz#58d4a70cc2daee2becf7d4521d65ea1782d6ec68" - integrity sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg== - dependencies: - "@typescript-eslint/scope-manager" "8.53.1" - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/typescript-estree" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" - debug "^4.4.3" - -"@typescript-eslint/project-service@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.53.1.tgz#4e47856a0b14a1ceb28b0294b4badef3be1e9734" - integrity sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog== - dependencies: - "@typescript-eslint/tsconfig-utils" "^8.53.1" - "@typescript-eslint/types" "^8.53.1" - debug "^4.4.3" - -"@typescript-eslint/scope-manager@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz#6c4b8c82cd45ae3b365afc2373636e166743a8fa" - integrity sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ== - dependencies: - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" - -"@typescript-eslint/tsconfig-utils@8.53.1", "@typescript-eslint/tsconfig-utils@^8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz#efe80b8d019cd49e5a1cf46c2eb0cd2733076424" - integrity sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA== - -"@typescript-eslint/type-utils@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz#95de2651a96d580bf5c6c6089ddd694284d558ad" - integrity sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w== - dependencies: - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/typescript-estree" "8.53.1" - "@typescript-eslint/utils" "8.53.1" - debug "^4.4.3" - ts-api-utils "^2.4.0" - -"@typescript-eslint/types@8.53.1", "@typescript-eslint/types@^8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.53.1.tgz#101f203f0807a63216cceceedb815fabe21d5793" - integrity sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A== - -"@typescript-eslint/typescript-estree@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz#b6dce2303c9e27e95b8dcd8c325868fff53e488f" - integrity sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg== - dependencies: - "@typescript-eslint/project-service" "8.53.1" - "@typescript-eslint/tsconfig-utils" "8.53.1" - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/visitor-keys" "8.53.1" - debug "^4.4.3" - minimatch "^9.0.5" - semver "^7.7.3" - tinyglobby "^0.2.15" - ts-api-utils "^2.4.0" - -"@typescript-eslint/utils@8.53.1", "@typescript-eslint/utils@^8.18.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.53.1.tgz#81fe6c343de288701b774f4d078382f567e6edaa" - integrity sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg== - dependencies: - "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.53.1" - "@typescript-eslint/types" "8.53.1" - "@typescript-eslint/typescript-estree" "8.53.1" - -"@typescript-eslint/visitor-keys@8.53.1": - version "8.53.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz#405f04959be22b9be364939af8ac19c3649b6eb7" - integrity sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg== - dependencies: - "@typescript-eslint/types" "8.53.1" - eslint-visitor-keys "^4.2.1" - -"@unrs/resolver-binding-android-arm-eabi@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81" - integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw== - -"@unrs/resolver-binding-android-arm64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f" - integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g== - -"@unrs/resolver-binding-darwin-arm64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf" - integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== - -"@unrs/resolver-binding-darwin-x64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc" - integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ== - -"@unrs/resolver-binding-freebsd-x64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b" - integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw== - -"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a" - integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw== - -"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3" - integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw== - -"@unrs/resolver-binding-linux-arm64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d" - integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ== - -"@unrs/resolver-binding-linux-arm64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0" - integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w== - -"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44" - integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA== - -"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9" - integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ== - -"@unrs/resolver-binding-linux-riscv64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165" - integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew== - -"@unrs/resolver-binding-linux-s390x-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94" - integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg== - -"@unrs/resolver-binding-linux-x64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz#36fb318eebdd690f6da32ac5e0499a76fa881935" - integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w== - -"@unrs/resolver-binding-linux-x64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6" - integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA== - -"@unrs/resolver-binding-wasm32-wasi@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d" - integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ== - dependencies: - "@napi-rs/wasm-runtime" "^0.2.11" - -"@unrs/resolver-binding-win32-arm64-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35" - integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw== - -"@unrs/resolver-binding-win32-ia32-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6" - integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ== - -"@unrs/resolver-binding-win32-x64-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" - integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== - -"@viselect/react@3.9.0": - version "3.9.0" - resolved "https://registry.yarnpkg.com/@viselect/react/-/react-3.9.0.tgz#d37bbb33d1759d625ba3be219ac299ddb9e67ccd" - integrity sha512-uz3ehdawjXwTUGnDKyNfmecviLAjUIG7cOQdeKcdXiSCHg9ZS12Vhr/j0shuHjX0xPvy9IqlbVuvzOHq77QDQw== - dependencies: - "@viselect/vanilla" "3.9.0" - -"@viselect/vanilla@3.9.0": - version "3.9.0" - resolved "https://registry.yarnpkg.com/@viselect/vanilla/-/vanilla-3.9.0.tgz#bddadfa2726ab9590edfe42dac0cd8e5cb01fd4e" - integrity sha512-E9eBgoi/crJ0SlZMAc+Yst7nU324LZ5LLvcXjzWEcrfllscdpTml2OLOKHC7O8Bbz19OybSLv6VexxnjlJrLxQ== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^8.1.1: - version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== - dependencies: - acorn "^8.11.0" - -acorn@^8.11.0, acorn@^8.12.1, acorn@^8.15.0, acorn@^8.4.1: - version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" - integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== - -ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - -anymatch@^3.0.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-hidden@^1.2.4: - version "1.2.6" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" - integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== - dependencies: - tslib "^2.0.0" - -aria-query@^5.3.1, aria-query@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" - integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== - -array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" - integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== - dependencies: - call-bound "^1.0.3" - is-array-buffer "^3.0.5" - -array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: - version "3.1.9" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" - integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-abstract "^1.24.0" - es-object-atoms "^1.1.1" - get-intrinsic "^1.3.0" - is-string "^1.1.1" - math-intrinsics "^1.1.0" - -array.prototype.findlast@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" - integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-shim-unscopables "^1.0.2" - -array.prototype.findlastindex@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" - integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-abstract "^1.23.9" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - es-shim-unscopables "^1.1.0" - -array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" - integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-shim-unscopables "^1.0.2" - -array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" - integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-shim-unscopables "^1.0.2" - -array.prototype.tosorted@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" - integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.3" - es-errors "^1.3.0" - es-shim-unscopables "^1.0.2" - -arraybuffer.prototype.slice@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" - integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - is-array-buffer "^3.0.4" - -ast-types-flow@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" - integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== - -async-function@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" - integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== - -async@^3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - -attr-accept@^2.2.4: - version "2.2.5" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e" - integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ== - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -axe-core@^4.10.0: - version "4.11.1" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.1.tgz#052ff9b2cbf543f5595028b583e4763b40c78ea7" - integrity sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A== - -axobject-query@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" - integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== - -babel-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" - integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== - dependencies: - "@jest/transform" "^29.7.0" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.6.3" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - -babel-plugin-istanbul@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" - integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^5.0.4" - test-exclude "^6.0.0" - -babel-plugin-jest-hoist@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" - integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.1.14" - "@types/babel__traverse" "^7.0.6" - -babel-preset-current-node-syntax@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" - integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - -babel-preset-jest@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" - integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== - dependencies: - babel-plugin-jest-hoist "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base-64@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" - integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg== - -baseline-browser-mapping@^2.9.0: - version "2.9.17" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz#9d6019766cd7eba738cb5f32c84b9f937cc87780" - integrity sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ== - -brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" - integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browserslist@^4.24.0: - version "4.28.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" - integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== - dependencies: - baseline-browser-mapping "^2.9.0" - caniuse-lite "^1.0.30001759" - electron-to-chromium "^1.5.263" - node-releases "^2.0.27" - update-browserslist-db "^1.2.0" - -bs-logger@^0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" - integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== - dependencies: - fast-json-stable-stringify "2.x" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.7, call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" - integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== - dependencies: - call-bind-apply-helpers "^1.0.2" - get-intrinsic "^1.3.0" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001759: - version "1.0.30001765" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz#4a78d8a797fd4124ebaab2043df942eb091648ee" - integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ== - -chalk@4.1.2, chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - -chokidar@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== - dependencies: - readdirp "^4.0.1" - -chromatic@11.28.2: - version "11.28.2" - resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.28.2.tgz#c0b9f59dcc323ca8bf2c2790f3c9e3d285834efb" - integrity sha512-aCmUPcZUs4/p9zRZdMreOoO/5JqO2DiJC3md1/vRx8dlMRcmR/YI5ZbgXZcai2absVR+6hsXZ5XiPxV2sboTuQ== - -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -cjs-module-lexer@^1.0.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" - integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== - -classnames@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== - -client-only@0.0.1, client-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -clsx@2.1.1, clsx@^2.0.0, clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - -cmdk@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.0.4.tgz#cbddef6f5ade2378f85c80a0b9ad9a8a712779b5" - integrity sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg== - dependencies: - "@radix-ui/react-dialog" "^1.1.2" - "@radix-ui/react-id" "^1.1.0" - "@radix-ui/react-primitive" "^2.0.0" - use-sync-external-store "^1.2.2" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - -collect-v8-coverage@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz#cc1f01eb8d02298cbc9a437c74c70ab4e5210b80" - integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -commander@13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46" - integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw== - -compute-scroll-into-view@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa" - integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -core-js@^3: - version "3.48.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.48.0.tgz#1f813220a47bbf0e667e3885c36cd6f0593bf14d" - integrity sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ== - -create-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" - integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-config "^29.7.0" - jest-util "^29.7.0" - prompts "^2.0.1" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-fetch@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.1.0.tgz#8f69355007ee182e47fa692ecbaa37a52e43c3d2" - integrity sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw== - dependencies: - node-fetch "^2.7.0" - -cross-spawn@^7.0.3, cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -csstype@^3.0.2, csstype@^3.2.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" - integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== - -damerau-levenshtein@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== - -data-view-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" - integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-data-view "^1.0.2" - -data-view-byte-length@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" - integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-data-view "^1.0.2" - -data-view-byte-offset@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" - integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -date-fns@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" - integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== - -debug@4.4.3, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: - version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decimal.js@^10.4.3: - version "10.6.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" - integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== - -dedent@^1.0.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.1.tgz#364661eea3d73f3faba7089214420ec2f8f13e15" - integrity sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg== - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -deepmerge@4.3.1, deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.1.3, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -detect-libc@^2.0.3, detect-libc@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" - integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== - -detect-newline@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - -detect-node-es@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" - integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== - -devalue@^5.6.2: - version "5.6.2" - resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.6.2.tgz#931e2bb1cc2b299e0f0fb9e4e5be8ebf521a25b8" - integrity sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg== - -diff-sequences@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" - integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== - -diff@^4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" - integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== - -dnd-core@14.0.1: - version "14.0.1" - resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e" - integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A== - dependencies: - "@react-dnd/asap" "^4.0.0" - "@react-dnd/invariant" "^2.0.0" - redux "^4.1.1" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -downshift@9.0.9: - version "9.0.9" - resolved "https://registry.yarnpkg.com/downshift/-/downshift-9.0.9.tgz#fd683a376a21bcc52f0d13d6cda47b4ea521f62b" - integrity sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g== - dependencies: - "@babel/runtime" "^7.24.5" - compute-scroll-into-view "^3.1.0" - prop-types "^15.8.1" - react-is "18.2.0" - tslib "^2.6.2" - -dunder-proto@^1.0.0, dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -ejs@^3.1.10: - version "3.1.10" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" - integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== - dependencies: - jake "^10.8.5" - -electron-to-chromium@^1.5.263: - version "1.5.267" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" - integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== - -emittery@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" - integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -error-ex@^1.3.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" - integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0, es-abstract@^1.24.1: - version "1.24.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.1.tgz#f0c131ed5ea1bb2411134a8dd94def09c46c7899" - integrity sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw== - dependencies: - array-buffer-byte-length "^1.0.2" - arraybuffer.prototype.slice "^1.0.4" - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - data-view-buffer "^1.0.2" - data-view-byte-length "^1.0.2" - data-view-byte-offset "^1.0.1" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - es-set-tostringtag "^2.1.0" - es-to-primitive "^1.3.0" - function.prototype.name "^1.1.8" - get-intrinsic "^1.3.0" - get-proto "^1.0.1" - get-symbol-description "^1.1.0" - globalthis "^1.0.4" - gopd "^1.2.0" - has-property-descriptors "^1.0.2" - has-proto "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - internal-slot "^1.1.0" - is-array-buffer "^3.0.5" - is-callable "^1.2.7" - is-data-view "^1.0.2" - is-negative-zero "^2.0.3" - is-regex "^1.2.1" - is-set "^2.0.3" - is-shared-array-buffer "^1.0.4" - is-string "^1.1.1" - is-typed-array "^1.1.15" - is-weakref "^1.1.1" - math-intrinsics "^1.1.0" - object-inspect "^1.13.4" - object-keys "^1.1.1" - object.assign "^4.1.7" - own-keys "^1.0.1" - regexp.prototype.flags "^1.5.4" - safe-array-concat "^1.1.3" - safe-push-apply "^1.0.0" - safe-regex-test "^1.1.0" - set-proto "^1.0.0" - stop-iteration-iterator "^1.1.0" - string.prototype.trim "^1.2.10" - string.prototype.trimend "^1.0.9" - string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.3" - typed-array-byte-length "^1.0.3" - typed-array-byte-offset "^1.0.4" - typed-array-length "^1.0.7" - unbox-primitive "^1.1.0" - which-typed-array "^1.1.19" - -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-iterator-helpers@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz#d979a9f686e2b0b72f88dbead7229924544720bc" - integrity sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-abstract "^1.24.1" - es-errors "^1.3.0" - es-set-tostringtag "^2.1.0" - function-bind "^1.1.2" - get-intrinsic "^1.3.0" - globalthis "^1.0.4" - gopd "^1.2.0" - has-property-descriptors "^1.0.2" - has-proto "^1.2.0" - has-symbols "^1.1.0" - internal-slot "^1.1.0" - iterator.prototype "^1.1.5" - safe-array-concat "^1.1.3" - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" - integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" - integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== - dependencies: - hasown "^2.0.2" - -es-to-primitive@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" - integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== - dependencies: - is-callable "^1.2.7" - is-date-object "^1.0.5" - is-symbol "^1.0.4" - -escalade@^3.1.1, escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-config-next@15.1.7: - version "15.1.7" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.1.7.tgz#80d287e6d1c0742304de8fe57cbc45000c455c11" - integrity sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw== - dependencies: - "@next/eslint-plugin-next" "15.1.7" - "@rushstack/eslint-patch" "^1.10.3" - "@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" - "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.31.0" - eslint-plugin-jsx-a11y "^6.10.0" - eslint-plugin-react "^7.37.0" - eslint-plugin-react-hooks "^5.0.0" - -eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: - version "0.3.9" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" - integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== - dependencies: - debug "^3.2.7" - is-core-module "^2.13.0" - resolve "^1.22.4" - -eslint-import-resolver-typescript@^3.5.2: - version "3.10.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz#23dac32efa86a88e2b8232eb244ac499ad636db2" - integrity sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ== - dependencies: - "@nolyfill/is-core-module" "1.0.39" - debug "^4.4.0" - get-tsconfig "^4.10.0" - is-bun-module "^2.0.0" - stable-hash "^0.0.5" - tinyglobby "^0.2.13" - unrs-resolver "^1.6.2" - -eslint-module-utils@^2.12.1: - version "2.12.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" - integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== - dependencies: - debug "^3.2.7" - -eslint-plugin-import@^2.31.0: - version "2.32.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" - integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== - dependencies: - "@rtsao/scc" "^1.1.0" - array-includes "^3.1.9" - array.prototype.findlastindex "^1.2.6" - array.prototype.flat "^1.3.3" - array.prototype.flatmap "^1.3.3" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.12.1" - hasown "^2.0.2" - is-core-module "^2.16.1" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.fromentries "^2.0.8" - object.groupby "^1.0.3" - object.values "^1.2.1" - semver "^6.3.1" - string.prototype.trimend "^1.0.9" - tsconfig-paths "^3.15.0" - -eslint-plugin-jsx-a11y@^6.10.0: - version "6.10.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" - integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== - dependencies: - aria-query "^5.3.2" - array-includes "^3.1.8" - array.prototype.flatmap "^1.3.2" - ast-types-flow "^0.0.8" - axe-core "^4.10.0" - axobject-query "^4.1.0" - damerau-levenshtein "^1.0.8" - emoji-regex "^9.2.2" - hasown "^2.0.2" - jsx-ast-utils "^3.3.5" - language-tags "^1.0.9" - minimatch "^3.1.2" - object.fromentries "^2.0.8" - safe-regex-test "^1.0.3" - string.prototype.includes "^2.0.1" - -eslint-plugin-react-hooks@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" - integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== - -eslint-plugin-react@^7.37.0: - version "7.37.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" - integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== - dependencies: - array-includes "^3.1.8" - array.prototype.findlast "^1.2.5" - array.prototype.flatmap "^1.3.3" - array.prototype.tosorted "^1.1.4" - doctrine "^2.1.0" - es-iterator-helpers "^1.2.1" - estraverse "^5.3.0" - hasown "^2.0.2" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.9" - object.fromentries "^2.0.8" - object.values "^1.2.1" - prop-types "^15.8.1" - resolve "^2.0.0-next.5" - semver "^6.3.1" - string.prototype.matchall "^4.0.12" - string.prototype.repeat "^1.0.0" - -eslint-scope@^8.2.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" - integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -eslint-visitor-keys@^4.2.0, eslint-visitor-keys@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" - integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== - -eslint@9.20.1: - version "9.20.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.20.1.tgz#923924c078f5226832449bac86662dd7e53c91d6" - integrity sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.19.0" - "@eslint/core" "^0.11.0" - "@eslint/eslintrc" "^3.2.0" - "@eslint/js" "9.20.0" - "@eslint/plugin-kit" "^0.2.5" - "@humanfs/node" "^0.16.6" - "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.4.1" - "@types/estree" "^1.0.6" - "@types/json-schema" "^7.0.15" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.6" - debug "^4.3.2" - escape-string-regexp "^4.0.0" - eslint-scope "^8.2.0" - eslint-visitor-keys "^4.2.0" - espree "^10.3.0" - esquery "^1.5.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^8.0.0" - find-up "^5.0.0" - glob-parent "^6.0.2" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - json-stable-stringify-without-jsonify "^1.0.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - -esm-env@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/esm-env/-/esm-env-1.2.2.tgz#263c9455c55861f41618df31b20cb571fc20b75e" - integrity sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA== - -espree@^10.0.1, espree@^10.3.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" - integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== - dependencies: - acorn "^8.15.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.2.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.5.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" - integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== - dependencies: - estraverse "^5.1.0" - -esrap@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/esrap/-/esrap-2.2.2.tgz#437163734f470f6191580652388a4dc5e09643f6" - integrity sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -exenv@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" - integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw== - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - -expect@^29.0.0, expect@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" - integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== - dependencies: - "@jest/expect-utils" "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fastq@^1.6.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" - integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== - dependencies: - reusify "^1.0.4" - -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - -fdir@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" - integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== - -figlet@1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.8.1.tgz#e8e8a07e8c16be24c31086d7d5de8a9b9cf7f0fd" - integrity sha512-kEC3Sme+YvA8Hkibv0NR1oClGcWia0VB2fC1SlMy027cwe795Xx40Xiv/nw/iFAwQLupymWh+uhAAErn/7hwPg== - -file-entry-cache@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" - integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== - dependencies: - flat-cache "^4.0.0" - -file-selector@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4" - integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig== - dependencies: - tslib "^2.7.0" - -filelist@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" - integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== - dependencies: - minimatch "^5.0.1" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" - integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.4" - -flatted@^3.2.9: - version "3.3.3" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" - integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== - -for-each@^0.3.3, for-each@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" - integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== - dependencies: - is-callable "^1.2.7" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -fsevents@^2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" - integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - functions-have-names "^1.2.3" - hasown "^2.0.2" - is-callable "^1.2.7" - -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -generator-function@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" - integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-nonce@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" - integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== - -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - -get-proto@^1.0.0, get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-symbol-description@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" - integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - -get-tsconfig@^4.10.0: - version "4.13.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.0.tgz#fcdd991e6d22ab9a600f00e91c318707a5d9a0d7" - integrity sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ== - dependencies: - resolve-pkg-maps "^1.0.0" - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" - integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== - -globalthis@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" - integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== - dependencies: - define-properties "^1.2.1" - gopd "^1.0.1" - -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -has-bigints@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" - integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" - integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== - dependencies: - dunder-proto "^1.0.0" - -has-symbols@^1.0.3, has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-parse-stringify@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" - integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== - dependencies: - void-elements "3.1.0" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -i18next-browser-languagedetector@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz#c3ca311e249d2f7d8bb9b3b13ac9af380a3b15b0" - integrity sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g== - dependencies: - "@babel/runtime" "^7.23.2" - -i18next-fs-backend@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-2.6.1.tgz#bb352278136f47599783308df29f0a1b6a0d08b6" - integrity sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ== - -i18next@25.6.2: - version "25.6.2" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.6.2.tgz#d6de1a4be58242c7f0f1436ab2fa66c8f87b2e29" - integrity sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw== - dependencies: - "@babel/runtime" "^7.27.6" - -ical.js@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ical.js/-/ical.js-2.2.1.tgz#f06fe9b32498d948893ec994e6964379dac6a907" - integrity sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg== - -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -ignore@^7.0.5: - version "7.0.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" - integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== - -immutable@^5.0.2: - version "5.1.4" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.4.tgz#e3f8c1fe7b567d56cf26698f31918c241dae8c1f" - integrity sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA== - -import-fresh@^3.2.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" - integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-local@^3.0.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" - integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -internal-slot@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" - integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.2" - side-channel "^1.1.0" - -intl-messageformat@^10.1.0: - version "10.7.18" - resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.18.tgz#51a6f387afbca9b0f881b2ec081566db8c540b0d" - integrity sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g== - dependencies: - "@formatjs/ecma402-abstract" "2.3.6" - "@formatjs/fast-memoize" "2.2.7" - "@formatjs/icu-messageformat-parser" "2.11.4" - tslib "^2.8.0" - -is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" - integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - get-intrinsic "^1.2.6" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-async-function@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" - integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== - dependencies: - async-function "^1.0.0" - call-bound "^1.0.3" - get-proto "^1.0.1" - has-tostringtag "^1.0.2" - safe-regex-test "^1.1.0" - -is-bigint@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" - integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== - dependencies: - has-bigints "^1.0.2" - -is-boolean-object@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" - integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-bun-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" - integrity sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ== - dependencies: - semver "^7.7.1" - -is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-core-module@^2.13.0, is-core-module@^2.16.1: - version "2.16.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" - integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== - dependencies: - hasown "^2.0.2" - -is-data-view@^1.0.1, is-data-view@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" - integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== - dependencies: - call-bound "^1.0.2" - get-intrinsic "^1.2.6" - is-typed-array "^1.1.13" - -is-date-object@^1.0.5, is-date-object@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" - integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== - dependencies: - call-bound "^1.0.2" - has-tostringtag "^1.0.2" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-finalizationregistry@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" - integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== - dependencies: - call-bound "^1.0.3" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - -is-generator-function@^1.0.10: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" - integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== - dependencies: - call-bound "^1.0.4" - generator-function "^2.0.0" - get-proto "^1.0.1" - has-tostringtag "^1.0.2" - safe-regex-test "^1.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-map@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" - integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== - -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - -is-number-object@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" - integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-reference@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.3.tgz#9ef7bf9029c70a67b2152da4adf57c23d718910f" - integrity sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw== - dependencies: - "@types/estree" "^1.0.6" - -is-regex@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" - integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== - dependencies: - call-bound "^1.0.2" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - -is-set@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" - integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== - -is-shared-array-buffer@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" - integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== - dependencies: - call-bound "^1.0.3" - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" - integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== - dependencies: - call-bound "^1.0.3" - has-tostringtag "^1.0.2" - -is-symbol@^1.0.4, is-symbol@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" - integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== - dependencies: - call-bound "^1.0.2" - has-symbols "^1.1.0" - safe-regex-test "^1.1.0" - -is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" - integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== - dependencies: - which-typed-array "^1.1.16" - -is-weakmap@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" - integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== - -is-weakref@^1.0.2, is-weakref@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" - integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== - dependencies: - call-bound "^1.0.3" - -is-weakset@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" - integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== - dependencies: - call-bound "^1.0.3" - get-intrinsic "^1.2.6" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" - integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== - -istanbul-lib-instrument@^5.0.4: - version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== - dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.2.0" - semver "^6.3.0" - -istanbul-lib-instrument@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" - integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== - dependencies: - "@babel/core" "^7.23.9" - "@babel/parser" "^7.23.9" - "@istanbuljs/schema" "^0.1.3" - istanbul-lib-coverage "^3.2.0" - semver "^7.5.4" - -istanbul-lib-report@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" - integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^4.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-reports@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" - integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -iterator.prototype@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" - integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== - dependencies: - define-data-property "^1.1.4" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.6" - get-proto "^1.0.0" - has-symbols "^1.1.0" - set-function-name "^2.0.2" - -jake@^10.8.5: - version "10.9.4" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" - integrity sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA== - dependencies: - async "^3.2.6" - filelist "^1.0.4" - picocolors "^1.1.1" - -jest-changed-files@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" - integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== - dependencies: - execa "^5.0.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - -jest-circus@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" - integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^1.0.0" - is-generator-fn "^2.0.0" - jest-each "^29.7.0" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - pretty-format "^29.7.0" - pure-rand "^6.0.0" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-cli@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" - integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== - dependencies: - "@jest/core" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - chalk "^4.0.0" - create-jest "^29.7.0" - exit "^0.1.2" - import-local "^3.0.2" - jest-config "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - yargs "^17.3.1" - -jest-config@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" - integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== - dependencies: - "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.7.0" - "@jest/types" "^29.6.3" - babel-jest "^29.7.0" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-circus "^29.7.0" - jest-environment-node "^29.7.0" - jest-get-type "^29.6.3" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-runner "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - micromatch "^4.0.4" - parse-json "^5.2.0" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-json-comments "^3.1.1" - -jest-diff@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" - integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== - dependencies: - chalk "^4.0.0" - diff-sequences "^29.6.3" - jest-get-type "^29.6.3" - pretty-format "^29.7.0" - -jest-docblock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" - integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== - dependencies: - detect-newline "^3.0.0" - -jest-each@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" - integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - jest-get-type "^29.6.3" - jest-util "^29.7.0" - pretty-format "^29.7.0" - -jest-environment-node@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" - integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - jest-util "^29.7.0" - -jest-get-type@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" - integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== - -jest-haste-map@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" - integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== - dependencies: - "@jest/types" "^29.6.3" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - jest-worker "^29.7.0" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - -jest-leak-detector@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" - integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== - dependencies: - jest-get-type "^29.6.3" - pretty-format "^29.7.0" - -jest-matcher-utils@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" - integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== - dependencies: - chalk "^4.0.0" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - pretty-format "^29.7.0" - -jest-message-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" - integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.6.3" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-mock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" - integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-util "^29.7.0" - -jest-pnp-resolver@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== - -jest-regex-util@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" - integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== - -jest-resolve-dependencies@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" - integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== - dependencies: - jest-regex-util "^29.6.3" - jest-snapshot "^29.7.0" - -jest-resolve@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" - integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== - dependencies: - chalk "^4.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-pnp-resolver "^1.2.2" - jest-util "^29.7.0" - jest-validate "^29.7.0" - resolve "^1.20.0" - resolve.exports "^2.0.0" - slash "^3.0.0" - -jest-runner@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" - integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== - dependencies: - "@jest/console" "^29.7.0" - "@jest/environment" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.13.1" - graceful-fs "^4.2.9" - jest-docblock "^29.7.0" - jest-environment-node "^29.7.0" - jest-haste-map "^29.7.0" - jest-leak-detector "^29.7.0" - jest-message-util "^29.7.0" - jest-resolve "^29.7.0" - jest-runtime "^29.7.0" - jest-util "^29.7.0" - jest-watcher "^29.7.0" - jest-worker "^29.7.0" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runtime@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" - integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/globals" "^29.7.0" - "@jest/source-map" "^29.6.3" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-snapshot@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" - integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== - dependencies: - "@babel/core" "^7.11.6" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-jsx" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^29.7.0" - graceful-fs "^4.2.9" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - natural-compare "^1.4.0" - pretty-format "^29.7.0" - semver "^7.5.3" - -jest-util@^29.0.0, jest-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" - integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-validate@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" - integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== - dependencies: - "@jest/types" "^29.6.3" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^29.6.3" - leven "^3.1.0" - pretty-format "^29.7.0" - -jest-watcher@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" - integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== - dependencies: - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.13.1" - jest-util "^29.7.0" - string-length "^4.0.1" - -jest-worker@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" - integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== - dependencies: - "@types/node" "*" - jest-util "^29.7.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest@29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" - integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== - dependencies: - "@jest/core" "^29.7.0" - "@jest/types" "^29.6.3" - import-local "^3.0.2" - jest-cli "^29.7.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.2" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" - integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.1.0, js-yaml@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" - integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== - dependencies: - argparse "^2.0.1" - -jsesc@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" - integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json5@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: - version "3.3.5" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" - integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== - dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - object.assign "^4.1.4" - object.values "^1.1.6" - -keyv@^4.5.4: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -language-subtag-registry@^0.3.20: - version "0.3.23" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" - integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== - -language-tags@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" - integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== - dependencies: - language-subtag-registry "^0.3.20" - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -locate-character@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974" - integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA== - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -loose-envify@^1.0.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -magic-string@^0.30.11: - version "0.30.21" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" - integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.5" - -make-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" - integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== - dependencies: - semver "^7.5.3" - -make-error@^1.1.1, make-error@^1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -makeerror@1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" - integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== - dependencies: - tmpl "1.0.5" - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -"memoize-one@>=3.1.1 <6": - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimatch@*: - version "10.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" - integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== - dependencies: - "@isaacs/brace-expansion" "^5.0.0" - -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.5: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -ms@^2.1.1, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -nanoid@^3.3.6: - version "3.3.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" - integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== - -napi-postinstall@^0.3.0: - version "0.3.4" - resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz#7af256d6588b5f8e952b9190965d6b019653bbb9" - integrity sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -next-i18next@15.4.2: - version "15.4.2" - resolved "https://registry.yarnpkg.com/next-i18next/-/next-i18next-15.4.2.tgz#b2bc80ad77e92433e6bf2821bbb76c2630347495" - integrity sha512-zgRxWf7kdXtM686ecGIBQL+Bq0+DqAhRlasRZ3vVF0TmrNTWkVhs52n//oU3Fj5O7r/xOKkECDUwfOuXVwTK/g== - dependencies: - "@babel/runtime" "^7.23.2" - "@types/hoist-non-react-statics" "^3.3.6" - core-js "^3" - hoist-non-react-statics "^3.3.2" - i18next-fs-backend "^2.6.0" - -next@15.4.9: - version "15.4.9" - resolved "https://registry.yarnpkg.com/next/-/next-15.4.9.tgz#9204500a9c3cb2b981fb932077abfa66d3ef663f" - integrity sha512-/NedNmbiH4Umt/7SohT9VKxEdBt02Lj33xHrukE7d8Li6juT+75sEq4a4U06jggrvdbklKgr78OZEyWZ8XRrtw== - dependencies: - "@next/env" "15.4.9" - "@swc/helpers" "0.5.15" - caniuse-lite "^1.0.30001579" - postcss "8.4.31" - styled-jsx "5.1.6" - optionalDependencies: - "@next/swc-darwin-arm64" "15.4.8" - "@next/swc-darwin-x64" "15.4.8" - "@next/swc-linux-arm64-gnu" "15.4.8" - "@next/swc-linux-arm64-musl" "15.4.8" - "@next/swc-linux-x64-gnu" "15.4.8" - "@next/swc-linux-x64-musl" "15.4.8" - "@next/swc-win32-arm64-msvc" "15.4.8" - "@next/swc-win32-x64-msvc" "15.4.8" - sharp "^0.34.3" - -node-addon-api@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" - integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== - -node-fetch@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-releases@^2.0.27: - version "2.0.27" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" - integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== - -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.3, object-inspect@^1.13.4: - version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" - integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.4, object.assign@^4.1.7: - version "4.1.7" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" - integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - has-symbols "^1.1.0" - object-keys "^1.1.1" - -object.entries@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" - integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.4" - define-properties "^1.2.1" - es-object-atoms "^1.1.1" - -object.fromentries@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" - integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.groupby@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" - integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - -object.values@^1.1.6, object.values@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" - integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.5" - -own-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" - integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== - dependencies: - get-intrinsic "^1.2.6" - object-keys "^1.1.1" - safe-push-apply "^1.0.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2, p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -picocolors@^1.0.0, picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" - integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== - -pirates@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" - integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -playwright-core@1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" - integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== - -playwright@1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" - integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== - dependencies: - playwright-core "1.56.1" - optionalDependencies: - fsevents "2.3.2" - -possible-typed-array-names@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" - integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== - -postcss@8.4.31: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -pretty-bytes@7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-7.1.0.tgz#d788c9906241dbdcd4defab51b6d7470243db9bd" - integrity sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw== - -pretty-format@^29.0.0, pretty-format@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - -prompts@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prop-types@^15.7.2, prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -pure-rand@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" - integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -react-arborist@3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/react-arborist/-/react-arborist-3.4.3.tgz#982791a07d1e279f279be88162c920112f3cee90" - integrity sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ== - dependencies: - react-dnd "^14.0.3" - react-dnd-html5-backend "^14.0.3" - react-window "^1.8.11" - redux "^5.0.0" - use-sync-external-store "^1.2.0" - -react-aria-components@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/react-aria-components/-/react-aria-components-1.8.0.tgz#db8744ceae1d117d06f73b3de29b716a8b4ef7c3" - integrity sha512-qNJ/Z4opj1/NKFf1ch/V8rNYar5MXu4J8YVAt2pFgnBRLjVlIlfnENN8Oa5OFiYFCzMPRFdq5mI8RuYIEnvZfg== - dependencies: - "@internationalized/date" "^3.8.0" - "@internationalized/string" "^3.2.6" - "@react-aria/autocomplete" "3.0.0-beta.2" - "@react-aria/collections" "3.0.0-rc.0" - "@react-aria/dnd" "^3.9.2" - "@react-aria/focus" "^3.20.2" - "@react-aria/interactions" "^3.25.0" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/overlays" "^3.27.0" - "@react-aria/ssr" "^3.9.8" - "@react-aria/toolbar" "3.0.0-beta.15" - "@react-aria/utils" "^3.28.2" - "@react-aria/virtualizer" "^4.1.4" - "@react-stately/autocomplete" "3.0.0-beta.1" - "@react-stately/layout" "^4.2.2" - "@react-stately/selection" "^3.20.1" - "@react-stately/table" "^3.14.1" - "@react-stately/utils" "^3.10.6" - "@react-stately/virtualizer" "^4.3.2" - "@react-types/form" "^3.7.11" - "@react-types/grid" "^3.3.1" - "@react-types/shared" "^3.29.0" - "@react-types/table" "^3.12.0" - "@swc/helpers" "^0.5.0" - client-only "^0.0.1" - react-aria "^3.39.0" - react-stately "^3.37.0" - use-sync-external-store "^1.4.0" - -react-aria@3.39.0: - version "3.39.0" - resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.39.0.tgz#68e01a25365f403cfbc870b7ec29093d8722f0ba" - integrity sha512-zXCjR01WnfW4uW0f294uWrvdfwEMHgDFSwMwMBwRafAvmsQea87X5VTAfDmQOAbPa+iQFcngIyH0Pn5CfXNrjw== - dependencies: - "@internationalized/string" "^3.2.6" - "@react-aria/breadcrumbs" "^3.5.23" - "@react-aria/button" "^3.13.0" - "@react-aria/calendar" "^3.8.0" - "@react-aria/checkbox" "^3.15.4" - "@react-aria/color" "^3.0.6" - "@react-aria/combobox" "^3.12.2" - "@react-aria/datepicker" "^3.14.2" - "@react-aria/dialog" "^3.5.24" - "@react-aria/disclosure" "^3.0.4" - "@react-aria/dnd" "^3.9.2" - "@react-aria/focus" "^3.20.2" - "@react-aria/gridlist" "^3.12.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/landmark" "^3.0.2" - "@react-aria/link" "^3.8.0" - "@react-aria/listbox" "^3.14.3" - "@react-aria/menu" "^3.18.2" - "@react-aria/meter" "^3.4.22" - "@react-aria/numberfield" "^3.11.13" - "@react-aria/overlays" "^3.27.0" - "@react-aria/progress" "^3.4.22" - "@react-aria/radio" "^3.11.2" - "@react-aria/searchfield" "^3.8.3" - "@react-aria/select" "^3.15.4" - "@react-aria/selection" "^3.24.0" - "@react-aria/separator" "^3.4.8" - "@react-aria/slider" "^3.7.18" - "@react-aria/ssr" "^3.9.8" - "@react-aria/switch" "^3.7.2" - "@react-aria/table" "^3.17.2" - "@react-aria/tabs" "^3.10.2" - "@react-aria/tag" "^3.5.2" - "@react-aria/textfield" "^3.17.2" - "@react-aria/toast" "^3.0.2" - "@react-aria/tooltip" "^3.8.2" - "@react-aria/tree" "^3.0.2" - "@react-aria/utils" "^3.28.2" - "@react-aria/visually-hidden" "^3.8.22" - "@react-types/shared" "^3.29.0" - -react-aria@^3.39.0: - version "3.45.0" - resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.45.0.tgz#58c432be5b9b582e4454832b4120379c377df156" - integrity sha512-QsdWIhhm3+IAiW3SU9tEm7pmeIcveEPAO6riZ1IUF78ZCvH/47nU4zVztcdtYmwYWSL4168QxLncWKtlMva3BA== - dependencies: - "@internationalized/string" "^3.2.7" - "@react-aria/breadcrumbs" "^3.5.30" - "@react-aria/button" "^3.14.3" - "@react-aria/calendar" "^3.9.3" - "@react-aria/checkbox" "^3.16.3" - "@react-aria/color" "^3.1.3" - "@react-aria/combobox" "^3.14.1" - "@react-aria/datepicker" "^3.15.3" - "@react-aria/dialog" "^3.5.32" - "@react-aria/disclosure" "^3.1.1" - "@react-aria/dnd" "^3.11.4" - "@react-aria/focus" "^3.21.3" - "@react-aria/gridlist" "^3.14.2" - "@react-aria/i18n" "^3.12.14" - "@react-aria/interactions" "^3.26.0" - "@react-aria/label" "^3.7.23" - "@react-aria/landmark" "^3.0.8" - "@react-aria/link" "^3.8.7" - "@react-aria/listbox" "^3.15.1" - "@react-aria/menu" "^3.19.4" - "@react-aria/meter" "^3.4.28" - "@react-aria/numberfield" "^3.12.3" - "@react-aria/overlays" "^3.31.0" - "@react-aria/progress" "^3.4.28" - "@react-aria/radio" "^3.12.3" - "@react-aria/searchfield" "^3.8.10" - "@react-aria/select" "^3.17.1" - "@react-aria/selection" "^3.27.0" - "@react-aria/separator" "^3.4.14" - "@react-aria/slider" "^3.8.3" - "@react-aria/ssr" "^3.9.10" - "@react-aria/switch" "^3.7.9" - "@react-aria/table" "^3.17.9" - "@react-aria/tabs" "^3.10.9" - "@react-aria/tag" "^3.7.3" - "@react-aria/textfield" "^3.18.3" - "@react-aria/toast" "^3.0.9" - "@react-aria/tooltip" "^3.9.0" - "@react-aria/tree" "^3.1.5" - "@react-aria/utils" "^3.32.0" - "@react-aria/visually-hidden" "^3.8.29" - "@react-types/shared" "^3.32.1" - -react-dnd-html5-backend@^14.0.3: - version "14.1.0" - resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f" - integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw== - dependencies: - dnd-core "14.0.1" - -react-dnd@^14.0.3: - version "14.0.5" - resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed" - integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A== - dependencies: - "@react-dnd/invariant" "^2.0.0" - "@react-dnd/shallowequal" "^2.0.0" - dnd-core "14.0.1" - fast-deep-equal "^3.1.3" - hoist-non-react-statics "^3.3.2" - -react-dom@19.2.0: - version "19.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8" - integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ== - dependencies: - scheduler "^0.27.0" - -react-dropzone@14.3.8: - version "14.3.8" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.8.tgz#a7eab118f8a452fe3f8b162d64454e81ba830582" - integrity sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug== - dependencies: - attr-accept "^2.2.4" - file-selector "^2.1.0" - prop-types "^15.8.1" - -react-hook-form@7.66.0: - version "7.66.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.66.0.tgz#1a09ea9d0ebb3bdda5073b08a486538d37d9c0d4" - integrity sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw== - -react-i18next@16.3.3: - version "16.3.3" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-16.3.3.tgz#098ff5443d0436a78692ca76303b2219aca32989" - integrity sha512-IaY2W+ueVd/fe7H6Wj2S4bTuLNChnajFUlZFfCTrTHWzGcOrUHlVzW55oXRSl+J51U8Onn6EvIhQ+Bar9FUcjw== - dependencies: - "@babel/runtime" "^7.27.6" - html-parse-stringify "^3.0.1" - use-sync-external-store "^1.6.0" - -react-is@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== - -react-is@^16.13.1, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - -react-lifecycles-compat@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-modal@3.16.3: - version "3.16.3" - resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.3.tgz#c412d41915782e3c261253435d01468e2439b11b" - integrity sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw== - dependencies: - exenv "^1.2.0" - prop-types "^15.7.2" - react-lifecycles-compat "^3.0.0" - warning "^4.0.3" - -react-remove-scroll-bar@^2.3.7: - version "2.3.8" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" - integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== - dependencies: - react-style-singleton "^2.2.2" - tslib "^2.0.0" - -react-remove-scroll@^2.6.3: - version "2.7.2" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz#6442da56791117661978ae99cd29be9026fecca0" - integrity sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q== - dependencies: - react-remove-scroll-bar "^2.3.7" - react-style-singleton "^2.2.3" - tslib "^2.1.0" - use-callback-ref "^1.3.3" - use-sidecar "^1.1.3" - -react-resizable-panels@2.1.7: - version "2.1.7" - resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7" - integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA== - -react-stately@3.37.0: - version "3.37.0" - resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.37.0.tgz#9bd09ecd1c7b11461ec60e17a7c670c17a64962e" - integrity sha512-fm2LRM3XN5lJD48+WQKWvESx54kAIHw0JztCRHMsFmTDgYWX/VASuXKON7LECv227stSEadrxGa8LhPkcelljw== - dependencies: - "@react-stately/calendar" "^3.8.0" - "@react-stately/checkbox" "^3.6.13" - "@react-stately/collections" "^3.12.3" - "@react-stately/color" "^3.8.4" - "@react-stately/combobox" "^3.10.4" - "@react-stately/data" "^3.12.3" - "@react-stately/datepicker" "^3.14.0" - "@react-stately/disclosure" "^3.0.3" - "@react-stately/dnd" "^3.5.3" - "@react-stately/form" "^3.1.3" - "@react-stately/list" "^3.12.1" - "@react-stately/menu" "^3.9.3" - "@react-stately/numberfield" "^3.9.11" - "@react-stately/overlays" "^3.6.15" - "@react-stately/radio" "^3.10.12" - "@react-stately/searchfield" "^3.5.11" - "@react-stately/select" "^3.6.12" - "@react-stately/selection" "^3.20.1" - "@react-stately/slider" "^3.6.3" - "@react-stately/table" "^3.14.1" - "@react-stately/tabs" "^3.8.1" - "@react-stately/toast" "^3.1.0" - "@react-stately/toggle" "^3.8.3" - "@react-stately/tooltip" "^3.5.3" - "@react-stately/tree" "^3.8.9" - "@react-types/shared" "^3.29.0" - -react-stately@^3.37.0: - version "3.43.0" - resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.43.0.tgz#30cda660cfc7430e84c4cc3d77e55e7b151d5bbd" - integrity sha512-dScb9fTL1tRtFODPnk/2rP0a9kp1C+7+40RArS0C7j0auAUmnrO/wDILojwQUso7/kkys4fP707fTwGJDeJ7vg== - dependencies: - "@react-stately/calendar" "^3.9.1" - "@react-stately/checkbox" "^3.7.3" - "@react-stately/collections" "^3.12.8" - "@react-stately/color" "^3.9.3" - "@react-stately/combobox" "^3.12.1" - "@react-stately/data" "^3.15.0" - "@react-stately/datepicker" "^3.15.3" - "@react-stately/disclosure" "^3.0.9" - "@react-stately/dnd" "^3.7.2" - "@react-stately/form" "^3.2.2" - "@react-stately/list" "^3.13.2" - "@react-stately/menu" "^3.9.9" - "@react-stately/numberfield" "^3.10.3" - "@react-stately/overlays" "^3.6.21" - "@react-stately/radio" "^3.11.3" - "@react-stately/searchfield" "^3.5.17" - "@react-stately/select" "^3.9.0" - "@react-stately/selection" "^3.20.7" - "@react-stately/slider" "^3.7.3" - "@react-stately/table" "^3.15.2" - "@react-stately/tabs" "^3.8.7" - "@react-stately/toast" "^3.1.2" - "@react-stately/toggle" "^3.9.3" - "@react-stately/tooltip" "^3.5.9" - "@react-stately/tree" "^3.9.4" - "@react-types/shared" "^3.32.1" - -react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" - integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== - dependencies: - get-nonce "^1.0.0" - tslib "^2.0.0" - -react-toastify@11.0.5: - version "11.0.5" - resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.5.tgz#ce4c42d10eeb433988ab2264d3e445c4e9d13313" - integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA== - dependencies: - clsx "^2.1.1" - -react-window@^1.8.11: - version "1.8.11" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525" - integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ== - dependencies: - "@babel/runtime" "^7.0.0" - memoize-one ">=3.1.1 <6" - -react@19.2.0: - version "19.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5" - integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ== - -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== - -redux@^4.1.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" - integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== - dependencies: - "@babel/runtime" "^7.9.2" - -redux@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" - integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== - -reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: - version "1.0.10" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" - integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-abstract "^1.23.9" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.7" - get-proto "^1.0.1" - which-builtin-type "^1.2.1" - -regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" - integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== - dependencies: - call-bind "^1.0.8" - define-properties "^1.2.1" - es-errors "^1.3.0" - get-proto "^1.0.1" - gopd "^1.2.0" - set-function-name "^2.0.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - -resolve.exports@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" - integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== - -resolve@^1.20.0, resolve@^1.22.4: - version "1.22.11" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" - integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== - dependencies: - is-core-module "^2.16.1" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^2.0.0-next.5: - version "2.0.0-next.5" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" - integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -reusify@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" - integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-array-concat@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" - integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - get-intrinsic "^1.2.6" - has-symbols "^1.1.0" - isarray "^2.0.5" - -safe-push-apply@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" - integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== - dependencies: - es-errors "^1.3.0" - isarray "^2.0.5" - -safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" - integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - is-regex "^1.2.1" - -sass@1.94.0: - version "1.94.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.94.0.tgz#a04198d8940358ca6ad537d2074051edbbe7c1a7" - integrity sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ== - dependencies: - chokidar "^4.0.0" - immutable "^5.0.2" - source-map-js ">=0.6.2 <2.0.0" - optionalDependencies: - "@parcel/watcher" "^2.4.1" - -sax@^1.2.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.4.tgz#f29c2bba80ce5b86f4343b4c2be9f2b96627cf8b" - integrity sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw== - -scheduler@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" - integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== - -semver@^6.3.0, semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@^7.7.1, semver@^7.7.3: - version "7.7.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" - integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== - -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -set-function-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" - integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.2" - -set-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" - integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== - dependencies: - dunder-proto "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - -sharp@^0.34.3: - version "0.34.5" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" - integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== - dependencies: - "@img/colour" "^1.0.0" - detect-libc "^2.1.2" - semver "^7.7.3" - optionalDependencies: - "@img/sharp-darwin-arm64" "0.34.5" - "@img/sharp-darwin-x64" "0.34.5" - "@img/sharp-libvips-darwin-arm64" "1.2.4" - "@img/sharp-libvips-darwin-x64" "1.2.4" - "@img/sharp-libvips-linux-arm" "1.2.4" - "@img/sharp-libvips-linux-arm64" "1.2.4" - "@img/sharp-libvips-linux-ppc64" "1.2.4" - "@img/sharp-libvips-linux-riscv64" "1.2.4" - "@img/sharp-libvips-linux-s390x" "1.2.4" - "@img/sharp-libvips-linux-x64" "1.2.4" - "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" - "@img/sharp-libvips-linuxmusl-x64" "1.2.4" - "@img/sharp-linux-arm" "0.34.5" - "@img/sharp-linux-arm64" "0.34.5" - "@img/sharp-linux-ppc64" "0.34.5" - "@img/sharp-linux-riscv64" "0.34.5" - "@img/sharp-linux-s390x" "0.34.5" - "@img/sharp-linux-x64" "0.34.5" - "@img/sharp-linuxmusl-arm64" "0.34.5" - "@img/sharp-linuxmusl-x64" "0.34.5" - "@img/sharp-wasm32" "0.34.5" - "@img/sharp-win32-arm64" "0.34.5" - "@img/sharp-win32-ia32" "0.34.5" - "@img/sharp-win32-x64" "0.34.5" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - -side-channel-map@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" - integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" - integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - side-channel-map "^1.0.1" - -side-channel@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" - integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - side-channel-list "^1.0.0" - side-channel-map "^1.0.1" - side-channel-weakmap "^1.0.2" - -signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - -source-map-support@0.5.13: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -stable-hash@^0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.5.tgz#94e8837aaeac5b4d0f631d2972adef2924b40269" - integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA== - -stack-utils@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" - integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== - dependencies: - escape-string-regexp "^2.0.0" - -stop-iteration-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" - integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== - dependencies: - es-errors "^1.3.0" - internal-slot "^1.1.0" - -string-length@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string.prototype.includes@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" - integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.3" - -string.prototype.matchall@^4.0.12: - version "4.0.12" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" - integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - define-properties "^1.2.1" - es-abstract "^1.23.6" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.6" - gopd "^1.2.0" - has-symbols "^1.1.0" - internal-slot "^1.1.0" - regexp.prototype.flags "^1.5.3" - set-function-name "^2.0.2" - side-channel "^1.1.0" - -string.prototype.repeat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" - integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trim@^1.2.10: - version "1.2.10" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" - integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - define-data-property "^1.1.4" - define-properties "^1.2.1" - es-abstract "^1.23.5" - es-object-atoms "^1.0.0" - has-property-descriptors "^1.0.2" - -string.prototype.trimend@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" - integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.2" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string.prototype.trimstart@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" - integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -styled-jsx@5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499" - integrity sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== - dependencies: - client-only "0.0.1" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svelte@^5.47.1: - version "5.48.0" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-5.48.0.tgz#ecd9273831d03ecf36518165eb51891bcc59a402" - integrity sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ== - dependencies: - "@jridgewell/remapping" "^2.3.4" - "@jridgewell/sourcemap-codec" "^1.5.0" - "@sveltejs/acorn-typescript" "^1.0.5" - "@types/estree" "^1.0.5" - acorn "^8.12.1" - aria-query "^5.3.1" - axobject-query "^4.1.0" - clsx "^2.1.1" - devalue "^5.6.2" - esm-env "^1.2.1" - esrap "^2.2.1" - is-reference "^3.0.3" - locate-character "^3.0.0" - magic-string "^0.30.11" - zimmerframe "^1.1.2" - -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -tinyglobby@^0.2.13, tinyglobby@^0.2.15: - version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== - dependencies: - fdir "^6.5.0" - picomatch "^4.0.3" - -tmpl@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" - integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -ts-api-utils@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" - integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== - -ts-ics@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/ts-ics/-/ts-ics-2.4.0.tgz#8247c809cc8675affe3bd2abae0634ed03e9aef6" - integrity sha512-Tguo+7W/swxTw8/Fxv4zPBqiHw/i1ohg0aCWeY50miwdwFGC7TCWcOUuqaeag5EdnaX57zn1qrqERsc4A+4j1A== - dependencies: - "@standard-schema/spec" "^1.0.0" - -ts-jest@29.2.5: - version "29.2.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63" - integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA== - dependencies: - bs-logger "^0.2.6" - ejs "^3.1.10" - fast-json-stable-stringify "^2.1.0" - jest-util "^29.0.0" - json5 "^2.2.3" - lodash.memoize "^4.1.2" - make-error "^1.3.6" - semver "^7.6.3" - yargs-parser "^21.1.1" - -ts-node@10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tsconfig-paths@^3.15.0: - version "3.15.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tsdav@2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/tsdav/-/tsdav-2.1.6.tgz#76f912b4a16ab0b84f03a6ecf38821e25f3a5d58" - integrity sha512-ngZCuhQvNClm5YHbuKN7EmRhOpu1XmsJ2+d56rpeiW9ZvXIxtDWyOf8TEojEgrgZVca9XJglVFNHYtyjQSmYOA== - dependencies: - base-64 "1.0.0" - cross-fetch "4.1.0" - debug "4.4.3" - xml-js "1.6.11" - -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.7.0, tslib@^2.8.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -turbo-darwin-64@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.6.1.tgz#62be8b3033c166103c6e6368bc37a29ebd9f5a1e" - integrity sha512-Dm0HwhyZF4J0uLqkhUyCVJvKM9Rw7M03v3J9A7drHDQW0qAbIGBrUijQ8g4Q9Cciw/BXRRd8Uzkc3oue+qn+ZQ== - -turbo-darwin-arm64@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.6.1.tgz#41482448cd40f226237d97ca1892572f93d488e5" - integrity sha512-U0PIPTPyxdLsrC3jN7jaJUwgzX5sVUBsKLO7+6AL+OASaa1NbT1pPdiZoTkblBAALLP76FM0LlnsVQOnmjYhyw== - -turbo-linux-64@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.6.1.tgz#1b47a28a3387858eb9c17ff77660180265074659" - integrity sha512-eM1uLWgzv89bxlK29qwQEr9xYWBhmO/EGiH22UGfq+uXr+QW1OvNKKMogSN65Ry8lElMH4LZh0aX2DEc7eC0Mw== - -turbo-linux-arm64@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.6.1.tgz#7cc9b42c70cc37f8799312c586a914bd8b78c944" - integrity sha512-MFFh7AxAQAycXKuZDrbeutfWM5Ep0CEZ9u7zs4Hn2FvOViTCzIfEhmuJou3/a5+q5VX1zTxQrKGy+4Lf5cdpsA== - -turbo-windows-64@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.6.1.tgz#c367296dd5f1465f1a1071ac987ba179b956531a" - integrity sha512-buq7/VAN7KOjMYi4tSZT5m+jpqyhbRU2EUTTvp6V0Ii8dAkY2tAAjQN1q5q2ByflYWKecbQNTqxmVploE0LVwQ== - -turbo-windows-arm64@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.6.1.tgz#530eafbcf46a2b7ed2f183a6a32a25a9c8901287" - integrity sha512-7w+AD5vJp3R+FB0YOj1YJcNcOOvBior7bcHTodqp90S3x3bLgpr7tE6xOea1e8JkP7GK6ciKVUpQvV7psiwU5Q== - -turbo@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.6.1.tgz#12107e157f49e6bb3fbcf54025d55fd7f30b9c7c" - integrity sha512-qBwXXuDT3rA53kbNafGbT5r++BrhRgx3sAo0cHoDAeG9g1ItTmUMgltz3Hy7Hazy1ODqNpR+C7QwqL6DYB52yA== - optionalDependencies: - turbo-darwin-64 "2.6.1" - turbo-darwin-arm64 "2.6.1" - turbo-linux-64 "2.6.1" - turbo-linux-arm64 "2.6.1" - turbo-windows-64 "2.6.1" - turbo-windows-arm64 "2.6.1" - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-detect@4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -typed-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" - integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-typed-array "^1.1.14" - -typed-array-byte-length@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" - integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== - dependencies: - call-bind "^1.0.8" - for-each "^0.3.3" - gopd "^1.2.0" - has-proto "^1.2.0" - is-typed-array "^1.1.14" - -typed-array-byte-offset@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" - integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - for-each "^0.3.3" - gopd "^1.2.0" - has-proto "^1.2.0" - is-typed-array "^1.1.15" - reflect.getprototypeof "^1.0.9" - -typed-array-length@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" - integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - is-typed-array "^1.1.13" - possible-typed-array-names "^1.0.0" - reflect.getprototypeof "^1.0.6" - -typescript@5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== - -unbox-primitive@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" - integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== - dependencies: - call-bound "^1.0.3" - has-bigints "^1.0.2" - has-symbols "^1.1.0" - which-boxed-primitive "^1.1.1" - -undici-types@~6.20.0: - version "6.20.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" - integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== - -undici-types@~7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" - integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== - -unrs-resolver@^1.6.2: - version "1.11.1" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" - integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg== - dependencies: - napi-postinstall "^0.3.0" - optionalDependencies: - "@unrs/resolver-binding-android-arm-eabi" "1.11.1" - "@unrs/resolver-binding-android-arm64" "1.11.1" - "@unrs/resolver-binding-darwin-arm64" "1.11.1" - "@unrs/resolver-binding-darwin-x64" "1.11.1" - "@unrs/resolver-binding-freebsd-x64" "1.11.1" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.11.1" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.11.1" - "@unrs/resolver-binding-linux-arm64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-arm64-musl" "1.11.1" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-riscv64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-riscv64-musl" "1.11.1" - "@unrs/resolver-binding-linux-s390x-gnu" "1.11.1" - "@unrs/resolver-binding-linux-x64-gnu" "1.11.1" - "@unrs/resolver-binding-linux-x64-musl" "1.11.1" - "@unrs/resolver-binding-wasm32-wasi" "1.11.1" - "@unrs/resolver-binding-win32-arm64-msvc" "1.11.1" - "@unrs/resolver-binding-win32-ia32-msvc" "1.11.1" - "@unrs/resolver-binding-win32-x64-msvc" "1.11.1" - -update-browserslist-db@^1.2.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" - integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -use-callback-ref@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" - integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== - dependencies: - tslib "^2.0.0" - -use-sidecar@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" - integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== - dependencies: - detect-node-es "^1.1.0" - tslib "^2.0.0" - -use-sync-external-store@^1.2.0, use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" - integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -v8-to-istanbul@^9.0.1: - version "9.3.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" - integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== - dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^2.0.0" - -void-elements@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" - integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== - -walker@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" - integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== - dependencies: - makeerror "1.0.12" - -warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" - integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== - dependencies: - is-bigint "^1.1.0" - is-boolean-object "^1.2.1" - is-number-object "^1.1.1" - is-string "^1.1.1" - is-symbol "^1.1.1" - -which-builtin-type@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" - integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== - dependencies: - call-bound "^1.0.2" - function.prototype.name "^1.1.6" - has-tostringtag "^1.0.2" - is-async-function "^2.0.0" - is-date-object "^1.1.0" - is-finalizationregistry "^1.1.0" - is-generator-function "^1.0.10" - is-regex "^1.2.1" - is-weakref "^1.0.2" - isarray "^2.0.5" - which-boxed-primitive "^1.1.0" - which-collection "^1.0.2" - which-typed-array "^1.1.16" - -which-collection@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" - integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== - dependencies: - is-map "^2.0.3" - is-set "^2.0.3" - is-weakmap "^2.0.2" - is-weakset "^2.0.3" - -which-typed-array@^1.1.16, which-typed-array@^1.1.19: - version "1.1.20" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" - integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - for-each "^0.3.5" - get-proto "^1.0.1" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^3.0.7" - -xml-js@1.6.11: - version "1.6.11" - resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" - integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== - dependencies: - sax "^1.2.4" - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.3.1: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zimmerframe@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/zimmerframe/-/zimmerframe-1.1.4.tgz#0352b5cafad3ad4526b0a526a9a52d9c040d865b" - integrity sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ== diff --git a/src/nginx/servers.conf.erb b/src/nginx/servers.conf.erb deleted file mode 100644 index 8beba22..0000000 --- a/src/nginx/servers.conf.erb +++ /dev/null @@ -1,97 +0,0 @@ -# ERB templated nginx configuration -# see https://doc.scalingo.com/platform/deployment/buildpacks/nginx - -upstream backend_server { - server localhost:8000 fail_timeout=0; -} - -server { - - listen <%= ENV["PORT"] %>; - server_name _; - - root /app/build/frontend-out; - - error_page 404 /404.html; - - # Django rest framework and external API - location ~ ^/(api|external_api)/ { - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_redirect off; - proxy_pass http://backend_server; - } - - # CalDAV well-known discovery - location = /.well-known/caldav { - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_redirect off; - proxy_pass http://backend_server; - } - - # Django views (RSVP, iCal export) - location ^~ /rsvp/ { - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_redirect off; - proxy_pass http://backend_server; - } - - location ^~ /ical/ { - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_redirect off; - proxy_pass http://backend_server; - } - - # Django admin - location ^~ /admin/ { - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_redirect off; - proxy_pass http://backend_server; - } - - location ~ "^/401/?$" { - try_files $uri /401.html; - } - - location ~ "^/403/?$" { - try_files $uri /403.html; - } - - location = /404.html { - internal; - } - - # Frontend export - location / { - try_files $uri $uri.html $uri/ =404; - } - -} - -# Internal CalDAV server (SabreDAV via PHP-FPM) -# Only accessible from localhost — Django proxies to this -server { - listen 127.0.0.1:9001; - server_name _; - - location / { - include fastcgi_params; - fastcgi_pass unix:/tmp/php-fpm.sock; - fastcgi_param SCRIPT_FILENAME /app/sabredav/server.php; - fastcgi_read_timeout 60; - } -} diff --git a/src/proxy/Caddyfile b/src/proxy/Caddyfile new file mode 100644 index 0000000..12b1e32 --- /dev/null +++ b/src/proxy/Caddyfile @@ -0,0 +1,95 @@ +{ + auto_https off + admin off +} + +:{$PORT} { + root * {$CALENDARS_FRONTEND_ROOT:/app} + header -Server + + route { + # Health checks + respond /__lbheartbeat__ 200 + respond /__lbheartbeat__/ 200 + + reverse_proxy /__heartbeat__/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + # Django backend proxy + reverse_proxy /api/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + reverse_proxy /external_api/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + # CalDAV proxy (Django proxies to SabreDAV internally) + reverse_proxy /caldav/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + reverse_proxy /caldav {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + # CalDAV well-known discovery + reverse_proxy /.well-known/caldav {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + # Django views (RSVP, iCal export) + reverse_proxy /rsvp/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + reverse_proxy /ical/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + # Django admin + redir /{$DJANGO_ADMIN_URL:admin} /{$DJANGO_ADMIN_URL:admin}/ 301 + + reverse_proxy /{$DJANGO_ADMIN_URL:admin}/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + # Django static files (admin CSS/JS) + reverse_proxy /static/* {$CALENDARS_FRONTEND_BACKEND_SERVER:localhost:8000} { + header_up X-Forwarded-Proto https + header_up X-Forwarded-For {remote_host} + } + + # Default: SPA fallback + try_files {path} /index.html + file_server + } + + handle_errors { + rewrite * /404.html + file_server + } +} + +# Internal CalDAV server (SabreDAV via PHP-FPM) +# Only accessible from localhost - Django proxies to this +http://localhost:9001 { + route { + reverse_proxy unix//tmp/php-fpm.sock { + transport fastcgi { + root /app/sabredav + env SCRIPT_FILENAME /app/sabredav/server.php + } + } + } +}