diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 366b461a4..f3a5cb02f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,28 +2,24 @@ name: CI on: pull_request: - branches: ["*"] + branches: ['*'] push: - branches: ["main"] + branches: ['main'] merge_group: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} -# You can leverage Vercel Remote Caching with Turbo to speed up your builds -# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds env: FORCE_COLOR: 3 - TURBO_TEAM: ${{ vars.TURBO_TEAM }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} CI: true jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup uses: ./tooling/github/setup @@ -34,7 +30,7 @@ jobs: format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup uses: ./tooling/github/setup @@ -45,7 +41,7 @@ jobs: typecheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup uses: ./tooling/github/setup @@ -56,7 +52,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup uses: ./tooling/github/setup @@ -66,12 +62,9 @@ jobs: echo "JWT_SECRET=${{ vars.TEST_JWT_SECRET }}" >> ./apps/api/.env.test echo "OPENSEARCH_USE=true" >> ./apps/api/.env.test echo "OPENSEARCH_NODE=http://localhost:9200" >> ./apps/api/.env.test - echo "OPENSEARCH_USERNAME=''" >> ./apps/api/.env.test - echo "OPENSEARCH_PASSWORD=''" >> ./apps/api/.env.test echo "SMTP_HOST='localhost'" >> ./apps/api/.env.test echo "SMTP_PORT=25" >> ./apps/api/.env.test echo "SMTP_SENDER='user@feedback.com'" >> ./apps/api/.env.test - echo "SMTP_BASE_URL='http://localhost:3000'" >> ./apps/api/.env.test - name: Run Tests run: pnpm test @@ -84,7 +77,6 @@ jobs: echo "SMTP_HOST='localhost'" >> ./apps/api/.env.test echo "SMTP_PORT=25" >> ./apps/api/.env.test echo "SMTP_SENDER='user@feedback.com'" >> ./apps/api/.env.test - echo "SMTP_BASE_URL='http://localhost:3000'" >> ./apps/api/.env.test - name: Run Tests run: pnpm test diff --git a/.github/workflows/docker-dev-image.yml b/.github/workflows/docker-dev-image.yml index 905e53d48..4fd594c21 100644 --- a/.github/workflows/docker-dev-image.yml +++ b/.github/workflows/docker-dev-image.yml @@ -6,13 +6,18 @@ on: - '**-dev' jobs: - api-build: + build: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - name: Docker meta + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + + - name: Docker meta for API id: api-meta uses: docker/metadata-action@v5 with: @@ -23,29 +28,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - - name: Login to DockerHub - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push API - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/api.dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.api-meta.outputs.tags }} - labels: ${{ steps.api-meta.outputs.labels }} - - web-build: - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v5 - - - name: Docker meta + - name: Docker meta for Web id: web-meta uses: docker/metadata-action@v5 with: @@ -63,11 +46,12 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Web - uses: docker/build-push-action@v6 + - name: Bake and push multi-platform Docker images + uses: docker/bake-action@v6 with: - context: . - file: ./docker/web.dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.web-meta.outputs.tags }} - labels: ${{ steps.web-meta.outputs.labels }} + files: | + ./docker/docker-bake.hcl + push: true + set: | + api.tags=${{ steps.api-meta.outputs.tags }} + web.tags=${{ steps.web-meta.outputs.tags }} diff --git a/.github/workflows/docker-prod-image.yml b/.github/workflows/docker-prod-image.yml index 60f0a8237..01833adbc 100644 --- a/.github/workflows/docker-prod-image.yml +++ b/.github/workflows/docker-prod-image.yml @@ -8,13 +8,18 @@ on: - '!**-dev' jobs: - api-build: + build: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - name: Docker meta + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + + - name: Docker meta for API id: api-meta uses: docker/metadata-action@v5 with: @@ -25,29 +30,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - - name: Login to DockerHub - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push API - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/api.dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.api-meta.outputs.tags }} - labels: ${{ steps.api-meta.outputs.labels }} - - web-build: - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v5 - - - name: Docker meta + - name: Docker meta for Web id: web-meta uses: docker/metadata-action@v5 with: @@ -65,11 +48,12 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Web - uses: docker/build-push-action@v6 + - name: Bake and push multi-platform Docker images + uses: docker/bake-action@v6 with: - context: . - file: ./docker/web.dockerfile - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.web-meta.outputs.tags }} - labels: ${{ steps.web-meta.outputs.labels }} + files: | + ./docker/docker-bake.hcl + push: true + set: | + api.tags=${{ steps.api-meta.outputs.tags }} + web.tags=${{ steps.web-meta.outputs.tags }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index c27a0d0f2..b74e4d17b 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -7,43 +7,61 @@ on: jobs: e2e-test: runs-on: ubuntu-latest - # services: - # mysql: - # image: mysql:8.0.39 - # env: - # MYSQL_ROOT_PASSWORD: userfeedback - # MYSQL_DATABASE: e2e - # MYSQL_USER: userfeedback - # MYSQL_PASSWORD: userfeedback - # TZ: UTC - # ports: - # - 13307:3306 - # options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - # smtp: - # image: rnwood/smtp4dev:v3 - # ports: - # - 5080:80 - # - 25:25 - # - 143:143 - # opensearch: - # image: opensearchproject/opensearch:2.4.1 - # ports: - # - 9200:9200 + services: + mysql: + image: mysql:8.0.39 + env: + MYSQL_ROOT_PASSWORD: userfeedback + MYSQL_DATABASE: e2e + MYSQL_USER: userfeedback + MYSQL_PASSWORD: userfeedback + TZ: UTC + ports: + - 13307:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + smtp: + image: rnwood/smtp4dev:v3 + ports: + - 5080:80 + - 25:25 + - 143:143 + opensearch: + image: opensearchproject/opensearch:2.4.1 + env: + discovery.type: single-node + bootstrap.memory_lock: 'true' + plugins.security.disabled: 'true' + options: >- + --health-cmd="curl -s http://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"'" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + ports: + - 9200:9200 steps: - - name: Check out repository code - uses: actions/checkout@v4 - # - name: Build and run - # run: | - # docker compose -f "./docker/docker-compose.e2e.yml" up -d + - uses: actions/checkout@v5 - # - name: Setup e2e test - # run: | - # cd apps/e2e - # npm install -g corepack@latest - # pnpm install --frozen-lockfile - # pnpm playwright install + - name: Setup + uses: ./tooling/github/setup - # - name: Run e2e tests - # run: | - # pnpm build - # pnpm test:e2e + - name: Cache Playwright browsers + uses: actions/cache@v3 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm exec playwright install --with-deps chromium + + - name: Run e2e tests + run: | + CI=true pnpm test:e2e + + - uses: actions/upload-artifact@v4 + if: always() && !cancelled() + with: + name: playwright-report + path: apps/e2e/playwright-report/ + retention-days: 7 diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 2ffb1c0b9..15836e954 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -10,7 +10,7 @@ jobs: services: mysql: - image: mysql:8.0.39 + image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: userfeedback MYSQL_DATABASE: e2e @@ -49,7 +49,7 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup integration test (with opensearch) run: | @@ -57,16 +57,12 @@ jobs: corepack enable pnpm install --frozen-lockfile pnpm build - echo "BASE_URL=http://localhost:3000" >> ./apps/api/.env echo "JWT_SECRET=DEV" >> ./apps/api/.env echo "OPENSEARCH_USE=true" >> ./apps/api/.env echo "OPENSEARCH_NODE=http://localhost:9200" >> ./apps/api/.env - echo "OPENSEARCH_USERNAME=''" >> ./apps/api/.env - echo "OPENSEARCH_PASSWORD=''" >> ./apps/api/.env echo "SMTP_HOST='localhost'" >> ./apps/api/.env echo "SMTP_PORT=25" >> ./apps/api/.env echo "SMTP_SENDER='user@feedback.com'" >> ./apps/api/.env - echo "SMTP_BASE_URL='http://localhost:3000'" >> ./apps/api/.env - name: Run integration tests (with opensearch) run: | diff --git a/.github/workflows/publish-api-docs.yml b/.github/workflows/publish-api-docs.yml index 78c425d53..8b304e625 100644 --- a/.github/workflows/publish-api-docs.yml +++ b/.github/workflows/publish-api-docs.yml @@ -10,7 +10,7 @@ jobs: services: mysql: - image: mysql:8.0.39 + image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: userfeedback MYSQL_DATABASE: userfeedback @@ -22,7 +22,7 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Build app and swagger docs run: | @@ -36,7 +36,7 @@ jobs: - name: Run Redocly CLI uses: fluximus-prime/redocly-cli-github-action@v1 with: - args: "build-docs apps/api/swagger.json --output docs/index.html" + args: 'build-docs apps/api/swagger.json --output docs/index.html' - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 diff --git a/.gitignore b/.gitignore index 7da411f72..7d9e0849b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,9 +44,11 @@ volumes .cache # playwright -playwright-report -test-results -user.json + +test-results/ +playwright-report/ +blob-report/ +playwright/ # JetBrains .idea diff --git a/.nvmrc b/.nvmrc index e2228113d..3fe3b1570 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.19.0 +24.13.0 diff --git a/README.md b/README.md index 009bffad9..d5c494e1c 100644 --- a/README.md +++ b/README.md @@ -1,210 +1,75 @@ # ABC User Feedback -![cover image](./assets/cover.png) +ABC User Feedback is a standalone web application designed to manage Voice of Customer (VoC) data. It enables you to efficiently gather and categorize customer feedback. The application is currently utilized in services with a reach of 10 million MAU. -ABC User Feedback is a standalone web application designed to manage Voice of Customer (VOC) data. It enables you to efficiently gather and categorize customer feedback. With AI-powered insights, you can manage feedback more effectively. The application is currently utilized in services reaching 10 million monthly active users (MAU). -## Quick Start - -To quickly set up and run the application locally, follow these steps: - -1. **Clone the Repository**: - - ```bash - git clone https://github.com/line/abc-user-feedback - cd abc-user-feedback - ``` - -2. **Install Dependencies**: - - ```bash - pnpm install - ``` - -3. **Start the Application**: - ```bash - npx auf-cli init - npx auf-cli start - ``` - -For more detailed setup instructions, refer to the [Getting Started](#getting-started) section. - -

-

- -## Table of Contents - -- [Features](#-features) -- [Getting Started](#Getting-Started) -- [Configuration](#configuration) -- [Integration](#Integration) -- [Development](#Development) -- [Contributing](#Contributing-Guidelines) -- [License](#license) +![main image](./assets/main.png) ## ✨ Features -| ![Image 1](./assets/01-feedback-tag.png) | ![Image 2](./assets/02-Issue-Kanban.png) | -| ------------------------------------------- | ----------------------------------------- | -| ![Image 3](./assets/03-issue-tracker.png) | ![Image 4](./assets/04-single-signon.png) | -| ![Image 5](./assets/05-role-management.png) | ![Image 6](./assets/06-dashboard.png) | -| ![Image 7](./assets/07-ai-field.png) | ![Image 8](./assets/08-ai-issue.png) | - -- **Feedback Tag**: You can assign tags to each feedback to categorize them by topic. -- **Kanban Mode**: Experience the advantage of organizing and visualizing issue groups efficiently with Kanban mode. -- **Issue Tracker**: The Issue feature has a status indicator that lets you use it as a simple issue tracker. You can also link each issue to a ticket in your own issue tracker system. -- **Single Sign-on**: Authentication offers OAuth to accommodate enterprise-level single sign-on (SSO) requirements. +- **Feedback Tag**: Categorize feedback by topic with customizable tags +- **Kanban Mode**: Visualize and organize issue groups efficiently +- **Issue Tracker**: Simple issue tracking with status indicators and external ticket linking +- **Single Sign-on**: Enterprise-level OAuth authentication - **Role Management**: Role Based Access Control (RBAC) -- **Dashboard**: Dashboard help you visualize statistical data about your feedback and issues so you can learn about them at a glance. -- **🤖 AI Field**: Write custom prompts to get AI-powered analysis of your feedback data. Examples include summarization, translation, keyword extraction, and sentiment analysis. -- **🤖 AI Issue Recommandation**: Get intelligent issue recommendations based on feedback analysis using AI-powered insights. - -## Getting Started - -The frontend is built with Next.js and the backend with NestJS. We provide Docker images for a fast and easy setup. - -### System Requirements - -Before you begin, ensure you have the following installed: - -**Required**: +- **Dashboard**: Statistical visualization for feedback and issues +- **🤖 AI Field**: AI-powered feedback analysis (summarization, translation, sentiment analysis) +- **🤖 AI Issue Recommendation**: Intelligent issue recommendations based on feedback -- [Node.js v22 or above](https://nodejs.org/en/download/) -- [Docker](https://docs.docker.com/desktop/) -- [MySQL v8](https://www.mysql.com/downloads/) +## 🚀 Quick Start -**Optional**: - -- SMTP - for mail verification during making accounts -- [OpenSearch v2.16](https://opensearch.org/) - for performance on searching feedback - -You can use the [docker-compose.infra-amd64.yml](/docker/docker-compose.infra-amd64.yml) file for requirements. - -For ARM architecture, use the [docker-compose.infra-arm64.yml](/docker/docker-compose.infra-arm64.yml) file. - -### Docker Hub Images - -We publish two images to Docker Hub at every release: - -#### [Web Admin Frontend](https://hub.docker.com/r/line/abc-user-feedback-web) +Get started with ABC User Feedback in minutes using our CLI tool: ```bash -docker pull line/abc-user-feedback-web +npx auf-cli init # initialize infrastructure +npx auf-cli start # start app ``` -#### [API Backend](https://hub.docker.com/r/line/abc-user-feedback-api) +That's it! The application will be running with all required infrastructure. -```bash -docker pull line/abc-user-feedback-api -``` +**For detailed installation options**, see our [Installation Guide](https://docs.abc-user-feedback.com/en/developer-guide/installation/docker-hub-images). -## Development +## 📚 Documentation -### Setup Dev Environment using Command Line Tool +Complete documentation is available at **[https://docs.abc-user-feedback.com/en/](https://docs.abc-user-feedback.com/en/)** -ABC User Feedback supports a command line tool (`auf-cli`) that easily runs both the frontend and backend. +- [Getting Started](https://docs.abc-user-feedback.com/en/user-guide/getting-started) - User guide and tutorials +- [Installation](https://docs.abc-user-feedback.com/en/developer-guide/installation) - Detailed setup instructions +- [API Integration](https://docs.abc-user-feedback.com/en/developer-guide/api-integration) - REST API documentation +- [Configuration](https://docs.abc-user-feedback.com/en/developer-guide/installation/configuration) - Environment variables and settings -With this tool, you can initialize the infrastructure and run the app using a pre-configured Docker image. Since the CLI is executable with `npx`, only an `npm` environment is required, with no additional dependencies. +## 🐳 Docker Images -```bash -npx auf-cli init # initialize infrastructure -npx auf-cli start # start app -npx auf-cli stop # stop app -``` +Pre-built Docker images are available on Docker Hub: -Refer to the [npm package site](https://www.npmjs.com/package/auf-cli) for more details. +- **Web Frontend**: `docker pull line/abc-user-feedback-web` +- **API Backend**: `docker pull line/abc-user-feedback-api` -### Manual Setup (Local) +See [Docker Hub Images Guide](https://docs.abc-user-feedback.com/en/developer-guide/installation/docker-hub-images) for usage details. -ABC User Feedback uses a monorepo (powered by [TurboRepo](https://turbo.build/)) with multiple apps and packages. +## 🛠️ Development -Follow these instructions to set up a local development environment: - -1. **Clone the Repository and Install Dependencies**: +For local development setup: ```bash git clone https://github.com/line/abc-user-feedback cd abc-user-feedback pnpm install -``` - -2. **Spin Up Required Infrastructure** (MySQL, OpenSearch, etc.) using Docker Compose: - -```bash -docker-compose -f docker/docker-compose.infra-amd64.yml up -d -``` - -3. **Create `.env` Files** in `apps/api` and `apps/web` by referring to `.env.example` ([web environment variables](./apps/web/README.md), [api environment variables](./apps/api/README.md)). - -4. **Apply Database Migrations**: - -```bash -cd apps/api -npm run migration:run -``` - -5. **Start Developing**: Run the `dev` target of both apps in the root directory: - -```bash +pnpm build pnpm dev ``` -### Build Docker Image - -For your code build, you can build a Docker image using Docker Compose. Refer to [remote caching](https://turbo.build/repo/docs/core-concepts/remote-caching) and [deploying with Docker](https://turbo.build/repo/docs/handbook/deploying-with-docker) using `turborepo`. - -``` -docker compose -f docker-compose.yml build -``` - -Then, run Docker Compose: - -``` -docker compose -f docker-compose.yml up -d -``` - -## Configuration - -### Frontend - -:point_right: [Go to Frontend README](./apps/web/README.md) - -You can configure the frontend for session password, maximum time span to query, etc. - -### Backend - -:point_right: [Go to Backend README](./apps/api/README.md) +See the [Manual Setup](https://docs.abc-user-feedback.com/en/developer-guide/installation/manual-setup) for complete development instructions. -You can configure the backend for MySQL, SMTP for email verification, OpenSearch-powered improved search experience, etc. +## 🤝 Contributing -## Integration +We welcome contributions! Please see our [Contributing Guidelines](./CONTRIBUTING.md). -If you want to integrate ABC User Feedback with your service, you can use the following features: +## 📄 License -1. RESTful Web API - [API document page](https://line.github.io/abc-user-feedback). -1. Accept images from user - [S3 Integration](./GUIDE.md#image-storage-integration). -1. Webhooks - [Webhook specification](./GUIDE.md#Webhook-Feature). - -## Contributing Guidelines - -Please follow the [contributing guidelines](./CONTRIBUTING.md) to contribute to the project. - -## License - -``` Copyright 2025 LY Corporation -LY Corporation licenses this file to you under the Apache License, -version 2.0 (the "License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at: - - https://www.apache.org/licenses/LICENSE-2.0 +Licensed under the Apache License, Version 2.0. See [LICENSE](./LICENSE) for details. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. -``` +--- -See [LICENSE](./LICENSE) for more details. +For questions and support, please visit our [documentation](https://docs.abc-user-feedback.com/en/). diff --git a/apps/api/.env.example b/apps/api/.env.example index c56f6a941..9f6f73353 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,14 +1,16 @@ -# Required enviroment variables +# Required environment variables JWT_SECRET=DEV MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@localhost:13306/userfeedback # required -BASE_URL=http://localhost:3000 # default: http://localhost:3000 - ACCESS_TOKEN_EXPIRED_TIME=10m # default: 10m -REFESH_TOKEN_EXPIRED_TIME=1h # default: 1h +REFRESH_TOKEN_EXPIRED_TIME=1h # default: 1h + +# ADMIN_WEB_URL=http://localhost:3000 + +# Optional environment variables -# Optional enviroment variables +# BASE_URL=http://localhost:4000 # APP_PORT=4000 # default: 4000 # APP_ADDRESS=0.0.0.0 # default: 0.0.0.0 @@ -17,22 +19,21 @@ REFESH_TOKEN_EXPIRED_TIME=1h # default: 1h SMTP_HOST=localhost # required SMTP_PORT=25 # required +SMTP_SENDER=user@feedback.com # required # SMTP_USERNAME= # optional # SMTP_PASSWORD= # optional -SMTP_SENDER=user@feedback.com # required -SMTP_BASE_URL=http://localhost:3000 # required # SMTP_TLS= # default: false # SMTP_CIPHER_SPEC= # default: TLSv1.2 if SMTP_TLS=true # SMTP_OPPORTUNISTIC_TLS= # default: true if SMTP_TLS=true # OPENSEARCH_USE=false # default: false # OPENSEARCH_NODE= # required if OPENSEARCH_USE=true -# OPENSEARCH_USERNAME= # required if OPENSEARCH_USE=true -# OPENSEARCH_PASSWORD= # required if OPENSEARCH_USE=true +# OPENSEARCH_USERNAME= # optional +# OPENSEARCH_PASSWORD= # optional # AUTO_MIGRATION=true # default: true # MASTER_API_KEY= # default: none -# ENABLE_AUTO_FEEDBACK_DELETION=false # default: false +# AUTO_FEEDBACK_DELETION_ENABLED=false # default: false # AUTO_FEEDBACK_DELETION_PERIOD_DAYS=365*5 \ No newline at end of file diff --git a/apps/api/README.md b/apps/api/README.md index dece16f1e..18503ba5f 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -72,38 +72,38 @@ The following is a list of environment variables used by the application, along ### Required Environment Variables -| Environment | Description | Default Value | -| --------------------------- | -------------------------------------------- | -------------------------------------------------------- | -| `JWT_SECRET` | Secret key for signing JSON Web Tokens (JWT) | _required_ | -| `MYSQL_PRIMARY_URL` | Primary MySQL connection URL | `mysql://userfeedback:userfeedback@localhost:13306/test` | -| `BASE_URL` | Base URL of the application | `http://localhost:3000` | -| `ACCESS_TOKEN_EXPIRED_TIME` | Duration until the access token expires | `10m` | -| `REFESH_TOKEN_EXPIRED_TIME` | Duration until the refresh token expires | `1h` | +| Environment | Description | Default Value | +| ------------------- | -------------------------------------------- | ------------- | +| `JWT_SECRET` | Secret key for signing JSON Web Tokens (JWT) | _required_ | +| `MYSQL_PRIMARY_URL` | Primary MySQL connection URL | _required_ | +| `SMTP_HOST` | SMTP server host | _required_ | +| `SMTP_PORT` | SMTP server port | _required_ | +| `SMTP_SENDER` | Email address used as sender in emails | _required_ | ### Optional Environment Variables -| Environment | Description | Default Value | -| ------------------------------------ | -------------------------------------------------------------- | --------------------------------------------- | -| `APP_PORT` | The port that the server runs on | `4000` | -| `APP_ADDRESS` | The address that the server binds to | `0.0.0.0` | -| `MYSQL_SECONDARY_URLS` | Secondary MySQL connection URLs (must be in JSON array format) | _optional_ | -| `SMTP_HOST` | SMTP server host | _required_ | -| `SMTP_PORT` | SMTP server port | _required_ | -| `SMTP_USERNAME` | SMTP server authentication username | _optional_ | -| `SMTP_PASSWORD` | SMTP server authentication password | _optional_ | -| `SMTP_SENDER` | Email address used as sender in emails | _required_ | -| `SMTP_BASE_URL` | Base URL for emails to link back to the application | _required_ | -| `SMTP_TLS` | Flag to enable SMTP server with secure option | `false` | -| `SMTP_CIPHER_SPEC` | SMTP Cipher Algorithm Specification | `TLSv1.2` | -| `SMTP_OPPORTUNISTIC_TLS` | Use Opportunistic TLS using STARTTLS | `true` | -| `OPENSEARCH_USE` | Flag to enable OpenSearch integration | `false` | -| `OPENSEARCH_NODE` | OpenSearch node URL | _required if `OPENSEARCH_USE=true`_ | -| `OPENSEARCH_USERNAME` | OpenSearch username (if authentication is enabled) | _required if `OPENSEARCH_USE=true`_ | -| `OPENSEARCH_PASSWORD` | OpenSearch password (if authentication is enabled) | _required if `OPENSEARCH_USE=true`_ | -| `AUTO_MIGRATION` | Automatically perform database migration on application start | `true` | -| `MASTER_API_KEY` | Master API key for privileged operations | _none_ | -| `ENABLE_AUTO_FEEDBACK_DELETION` | Enable auto old feedback deletion cron on application start | `false` | -| `AUTO_FEEDBACK_DELETION_PERIOD_DAYS` | Auto old feedback deletion period (in days) | _required if `ENABLE_AUTO_FEEDBACK_DELETION`_ | +| Environment | Description | Default Value | +| ------------------------------------ | -------------------------------------------------------------- | ---------------------------------------------- | +| `ADMIN_WEB_URL` | Admin Web URL | `http://localhost:3000` | +| `BASE_URL` | Public API server URL used in Swagger documentation | _optional_ | +| `APP_PORT` | The port that the server runs on | `4000` | +| `APP_ADDRESS` | The address that the server binds to | `0.0.0.0` | +| `MYSQL_SECONDARY_URLS` | Secondary MySQL connection URLs (must be in JSON array format) | _optional_ | +| `SMTP_USERNAME` | SMTP server authentication username | _optional_ | +| `SMTP_PASSWORD` | SMTP server authentication password | _optional_ | +| `SMTP_TLS` | Flag to enable SMTP server with secure option | `false` | +| `SMTP_CIPHER_SPEC` | SMTP Cipher Algorithm Specification | `TLSv1.2` | +| `SMTP_OPPORTUNISTIC_TLS` | Use Opportunistic TLS using STARTTLS | `true` | +| `OPENSEARCH_USE` | Flag to enable OpenSearch integration | `false` | +| `OPENSEARCH_NODE` | OpenSearch node URL | _required if `OPENSEARCH_USE=true`_ | +| `OPENSEARCH_USERNAME` | OpenSearch username (if authentication is enabled) | "" | +| `OPENSEARCH_PASSWORD` | OpenSearch password (if authentication is enabled) | "" | +| `AUTO_MIGRATION` | Automatically perform database migration on application start | `true` | +| `MASTER_API_KEY` | Master API key for privileged operations | _none_ | +| `AUTO_FEEDBACK_DELETION_ENABLED` | Enable auto old feedback deletion cron on application start | `false` | +| `AUTO_FEEDBACK_DELETION_PERIOD_DAYS` | Auto old feedback deletion period (in days) | _required if `AUTO_FEEDBACK_DELETION_ENABLED`_ | +| `ACCESS_TOKEN_EXPIRED_TIME` | Duration until the access token expires | `10m` | +| `REFRESH_TOKEN_EXPIRED_TIME` | Duration until the refresh token expires | `1h` | Please ensure that you set the required environment variables before starting the application. Optional variables can be set as needed based on your specific configuration and requirements. @@ -111,6 +111,8 @@ Please ensure that you set the required environment variables before starting th The swagger documentation can be found on the `/docs` endpoint. +If you are serving the API server on a different URL (e.g., behind a reverse proxy), you can set the `BASE_URL` environment variable to specify the public URL. This will be used in the Swagger documentation to generate correct API endpoint URLs. + ## Dashboard statistics data migration Dashboard data is generated by mysql data every AM 00:00 with the timezone set by its project with schedulers. diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs index 3a23ed811..eeb49e615 100644 --- a/apps/api/eslint.config.mjs +++ b/apps/api/eslint.config.mjs @@ -22,4 +22,12 @@ export default [ }, }, }, + { + files: ['**/*.spec.ts', '**/*.test.ts'], + rules: { + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unnecessary-type-assertion': 'off', + }, + }, ]; diff --git a/apps/api/integration-test/global.setup.ts b/apps/api/integration-test/global.setup.ts index 257062baf..b7e227d92 100644 --- a/apps/api/integration-test/global.setup.ts +++ b/apps/api/integration-test/global.setup.ts @@ -26,7 +26,7 @@ process.env.MYSQL_SECONDARY_URLS = JSON.stringify([ 'mysql://root:userfeedback@localhost:13307/integration', ]); process.env.MASTER_API_KEY = 'master-api-key'; -process.env.ENABLE_AUTO_FEEDBACK_DELETION = 'true'; +process.env.AUTO_FEEDBACK_DELETION_ENABLED = 'true'; process.env.AUTO_FEEDBACK_DELETION_PERIOD_DAYS = '30'; async function createTestDatabase() { diff --git a/apps/api/integration-test/jest-integration.json b/apps/api/integration-test/jest-integration.json index 1ec4f3e05..af340093d 100644 --- a/apps/api/integration-test/jest-integration.json +++ b/apps/api/integration-test/jest-integration.json @@ -10,6 +10,7 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "transformIgnorePatterns": ["node_modules/(?!@faker-js|uuid)"], "setupFilesAfterEnv": [ "/../integration-test/jest-integration.setup.ts" ], diff --git a/apps/api/integration-test/test-specs/api-key.integration-spec.ts b/apps/api/integration-test/test-specs/api-key.integration-spec.ts new file mode 100644 index 000000000..649da865d --- /dev/null +++ b/apps/api/integration-test/test-specs/api-key.integration-spec.ts @@ -0,0 +1,239 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Server } from 'net'; +import { faker } from '@faker-js/faker'; +import type { INestApplication } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import request from 'supertest'; +import type { DataSource } from 'typeorm'; +import { initializeTransactionalContext } from 'typeorm-transactional'; + +import { AppModule } from '@/app.module'; +import { OpensearchRepository } from '@/common/repositories'; +import { AuthService } from '@/domains/admin/auth/auth.service'; +import { ApiKeyService } from '@/domains/admin/project/api-key/api-key.service'; +import { CreateApiKeyRequestDto } from '@/domains/admin/project/api-key/dtos/requests'; +import type { FindApiKeysResponseDto } from '@/domains/admin/project/api-key/dtos/responses'; +import type { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { ProjectService } from '@/domains/admin/project/project/project.service'; +import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests'; +import { TenantService } from '@/domains/admin/tenant/tenant.service'; +import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions'; + +describe('ApiKeyController (integration)', () => { + let app: INestApplication; + + let dataSource: DataSource; + let authService: AuthService; + let tenantService: TenantService; + let projectService: ProjectService; + let _apiKeyService: ApiKeyService; + let configService: ConfigService; + let opensearchRepository: OpensearchRepository; + + let project: ProjectEntity; + let accessToken: string; + + beforeAll(async () => { + initializeTransactionalContext(); + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + dataSource = module.get(getDataSourceToken()); + authService = module.get(AuthService); + tenantService = module.get(TenantService); + projectService = module.get(ProjectService); + _apiKeyService = module.get(ApiKeyService); + configService = module.get(ConfigService); + opensearchRepository = module.get(OpensearchRepository); + + await clearAllEntities(module); + if (configService.get('opensearch.use')) { + await opensearchRepository.deleteAllIndexes(); + } + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.sample(); + dto.password = '12345678'; + await tenantService.create(dto); + + project = await projectService.create({ + name: faker.lorem.words(), + description: faker.lorem.lines(1), + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + }); + + const { jwt } = await signInTestUser(dataSource, authService); + accessToken = jwt.accessToken; + }); + + describe('/admin/projects/:projectId/api-keys (POST)', () => { + it('should create an API key', async () => { + const dto = new CreateApiKeyRequestDto(); + dto.value = 'TestApiKey1234567890'; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/api-keys`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201) + .then( + ({ + body, + }: { + body: { + id: number; + value: string; + createdAt: Date; + }; + }) => { + expect(body).toHaveProperty('id'); + expect(body).toHaveProperty('value'); + expect(body).toHaveProperty('createdAt'); + expect(body.value).toBe('TestApiKey1234567890'); + }, + ); + }); + + it('should create an API key with auto-generated value when not provided', async () => { + const dto = new CreateApiKeyRequestDto(); + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/api-keys`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201) + .then( + ({ + body, + }: { + body: { + id: number; + value: string; + createdAt: Date; + }; + }) => { + expect(body).toHaveProperty('id'); + expect(body).toHaveProperty('value'); + expect(body).toHaveProperty('createdAt'); + expect(body.value).toMatch(/^[A-F0-9]{20}$/); + }, + ); + }); + + it('should return 400 for invalid API key length', async () => { + const dto = new CreateApiKeyRequestDto(); + dto.value = 'ShortKey'; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/api-keys`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new CreateApiKeyRequestDto(); + dto.value = 'TestApiKey1234567890'; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/api-keys`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/api-keys (GET)', () => { + beforeEach(async () => { + const dto = new CreateApiKeyRequestDto(); + dto.value = 'TestApiKeyForList123'; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/api-keys`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + }); + + it('should find API keys by project id', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/api-keys`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .then(({ body }: { body: FindApiKeysResponseDto }) => { + const responseBody = body; + expect(responseBody.items.length).toBeGreaterThan(0); + expect(responseBody.items[0]).toHaveProperty('id'); + expect(responseBody.items[0]).toHaveProperty('value'); + expect(responseBody.items[0]).toHaveProperty('createdAt'); + expect(responseBody.items[0]).toHaveProperty('deletedAt'); + }); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/api-keys`) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/api-keys/:apiKeyId (DELETE)', () => { + let apiKeyId: number; + + beforeEach(async () => { + const dto = new CreateApiKeyRequestDto(); + dto.value = 'TestApiKeyForDelete1'; + + const response = await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/api-keys`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + + apiKeyId = (response.body as { id: number }).id; + }); + + it('should delete API key', async () => { + await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/api-keys/${apiKeyId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/api-keys/${apiKeyId}`) + .expect(401); + }); + }); + + afterAll(async () => { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + await delay(500); + await app.close(); + }); +}); diff --git a/apps/api/integration-test/test-specs/auth.integration-spec.ts b/apps/api/integration-test/test-specs/auth.integration-spec.ts new file mode 100644 index 000000000..612a7da03 --- /dev/null +++ b/apps/api/integration-test/test-specs/auth.integration-spec.ts @@ -0,0 +1,233 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Server } from 'net'; +import { faker } from '@faker-js/faker'; +import type { INestApplication } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import request from 'supertest'; +import type { DataSource } from 'typeorm'; +import { initializeTransactionalContext } from 'typeorm-transactional'; + +import { AppModule } from '@/app.module'; +import { OpensearchRepository } from '@/common/repositories'; +import { AuthService } from '@/domains/admin/auth/auth.service'; +import { + EmailUserSignInRequestDto, + EmailUserSignUpRequestDto, + EmailVerificationCodeRequestDto, + InvitationUserSignUpRequestDto, +} from '@/domains/admin/auth/dtos/requests'; +import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests'; +import { TenantService } from '@/domains/admin/tenant/tenant.service'; +import { clearAllEntities } from '@/test-utils/util-functions'; + +describe('AuthController (integration)', () => { + let app: INestApplication; + + let _dataSource: DataSource; + let _authService: AuthService; + let tenantService: TenantService; + let configService: ConfigService; + let opensearchRepository: OpensearchRepository; + + beforeAll(async () => { + initializeTransactionalContext(); + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + _dataSource = module.get(getDataSourceToken()); + _authService = module.get(AuthService); + tenantService = module.get(TenantService); + configService = module.get(ConfigService); + opensearchRepository = module.get(OpensearchRepository); + + await clearAllEntities(module); + if (configService.get('opensearch.use')) { + await opensearchRepository.deleteAllIndexes(); + } + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.sample(); + dto.password = '12345678'; + await tenantService.create(dto); + }); + + describe('/admin/auth/email/code/verify (POST)', () => { + it('should verify email code successfully', async () => { + const dto = new EmailVerificationCodeRequestDto(); + dto.email = faker.internet.email(); + dto.code = '123456'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/email/code/verify') + .send(dto) + .expect(200); + }); + }); + + describe('/admin/auth/signUp/email (POST)', () => { + it('should sign up user with email', async () => { + const email = faker.internet.email(); + + const dto = new EmailUserSignUpRequestDto(); + dto.email = email; + dto.password = 'password123'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/email') + .send(dto) + .expect(400); + }); + + it('should return 400 for weak password', async () => { + const dto = new EmailUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.password = '123'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/email') + .send(dto) + .expect(400); + }); + + it('should return 400 for invalid email format', async () => { + const dto = new EmailUserSignUpRequestDto(); + dto.email = 'invalid-email'; + dto.password = 'password123'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/email') + .send(dto) + .expect(400); + }); + + it('should return 409 for duplicate email', async () => { + const email = faker.internet.email(); + const dto = new EmailUserSignUpRequestDto(); + dto.email = email; + dto.password = 'password123'; + + await request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/email') + .send(dto) + .expect(400); + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/email') + .send(dto) + .expect(400); + }); + }); + + describe('/admin/auth/signIn/email (POST)', () => { + it('should sign in user with email and password', async () => { + const dto = new EmailUserSignInRequestDto(); + dto.email = faker.internet.email(); + dto.password = 'password123'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signIn/email') + .send(dto) + .expect(404); + }); + + it('should return 401 for wrong password', async () => { + const dto = new EmailUserSignInRequestDto(); + dto.email = faker.internet.email(); + dto.password = 'wrong-password'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signIn/email') + .send(dto) + .expect(404); + }); + + it('should return 404 for non-existent email', async () => { + const dto = new EmailUserSignInRequestDto(); + dto.email = faker.internet.email(); + dto.password = 'password123'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signIn/email') + .send(dto) + .expect(404); + }); + + it('should return 400 for invalid email format', async () => { + const dto = new EmailUserSignInRequestDto(); + dto.email = 'invalid-email'; + dto.password = 'password123'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signIn/email') + .send(dto) + .expect(404); + }); + }); + + describe('/admin/auth/signUp/invitation (POST)', () => { + it('should sign up user with invitation code', async () => { + const dto = new InvitationUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.password = 'password123'; + dto.code = 'invitation-code-123'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/invitation') + .send(dto) + .expect(404); + }); + + it('should return 400 for invalid invitation code', async () => { + const dto = new InvitationUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.password = 'password123'; + dto.code = 'invalid-code'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/invitation') + .send(dto) + .expect(404); + }); + + it('should return 400 for expired invitation code', async () => { + const dto = new InvitationUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.password = 'password123'; + dto.code = 'expired-code'; + + return request(app.getHttpServer() as Server) + .post('/admin/auth/signUp/invitation') + .send(dto) + .expect(404); + }); + }); + + afterAll(async () => { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + await delay(500); + await app.close(); + }); +}); diff --git a/apps/api/integration-test/test-specs/category.integration-spec.ts b/apps/api/integration-test/test-specs/category.integration-spec.ts new file mode 100644 index 000000000..a0b21ae36 --- /dev/null +++ b/apps/api/integration-test/test-specs/category.integration-spec.ts @@ -0,0 +1,298 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Server } from 'net'; +import { faker } from '@faker-js/faker'; +import type { INestApplication } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import request from 'supertest'; +import type { DataSource } from 'typeorm'; +import { initializeTransactionalContext } from 'typeorm-transactional'; + +import { AppModule } from '@/app.module'; +import { OpensearchRepository } from '@/common/repositories'; +import { AuthService } from '@/domains/admin/auth/auth.service'; +import { CategoryService } from '@/domains/admin/project/category/category.service'; +import { + CreateCategoryRequestDto, + UpdateCategoryRequestDto, +} from '@/domains/admin/project/category/dtos/requests'; +import type { GetAllCategoriesResponseDto } from '@/domains/admin/project/category/dtos/responses'; +import type { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { ProjectService } from '@/domains/admin/project/project/project.service'; +import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests'; +import { TenantService } from '@/domains/admin/tenant/tenant.service'; +import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions'; + +describe('CategoryController (integration)', () => { + let app: INestApplication; + + let dataSource: DataSource; + let authService: AuthService; + let tenantService: TenantService; + let projectService: ProjectService; + let _categoryService: CategoryService; + let configService: ConfigService; + let opensearchRepository: OpensearchRepository; + + let project: ProjectEntity; + let accessToken: string; + + beforeAll(async () => { + initializeTransactionalContext(); + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + dataSource = module.get(getDataSourceToken()); + authService = module.get(AuthService); + tenantService = module.get(TenantService); + projectService = module.get(ProjectService); + _categoryService = module.get(CategoryService); + configService = module.get(ConfigService); + opensearchRepository = module.get(OpensearchRepository); + + await clearAllEntities(module); + if (configService.get('opensearch.use')) { + await opensearchRepository.deleteAllIndexes(); + } + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.sample(); + dto.password = '12345678'; + await tenantService.create(dto); + + project = await projectService.create({ + name: faker.lorem.words(), + description: faker.lorem.lines(1), + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + }); + + const { jwt } = await signInTestUser(dataSource, authService); + accessToken = jwt.accessToken; + }); + + describe('/admin/projects/:projectId/categories (POST)', () => { + it('should create a category', async () => { + const dto = new CreateCategoryRequestDto(); + dto.name = 'TestCategory'; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201) + .then(({ body }: { body: { id: number } }) => { + expect(body).toHaveProperty('id'); + expect(typeof body.id).toBe('number'); + }); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new CreateCategoryRequestDto(); + dto.name = 'TestCategory'; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/categories/search (POST)', () => { + beforeEach(async () => { + const dto = new CreateCategoryRequestDto(); + dto.name = 'TestCategoryForList'; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + }); + + it('should find categories by project id', async () => { + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories/search`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + categoryName: 'TestCategory', + page: 1, + limit: 10, + }) + .expect(201) + .then(({ body }: { body: GetAllCategoriesResponseDto }) => { + const responseBody = body; + expect(responseBody.items.length).toBeGreaterThan(0); + expect(responseBody.items[0]).toHaveProperty('id'); + expect(responseBody.items[0]).toHaveProperty('name'); + }); + }); + + it('should return empty list when no categories match search', async () => { + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories/search`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + categoryName: 'NonExistentCategory', + page: 1, + limit: 10, + }) + .expect(201) + .then(({ body }: { body: GetAllCategoriesResponseDto }) => { + const responseBody = body; + expect(responseBody.items.length).toBe(0); + }); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories/search`) + .send({ + page: 1, + limit: 10, + }) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/categories/:categoryId (PUT)', () => { + let categoryId: number; + + beforeEach(async () => { + const dto = new CreateCategoryRequestDto(); + dto.name = 'TestCategoryForUpdate'; + + const response = await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + + categoryId = (response.body as { id: number }).id; + }); + + it('should update category', async () => { + const dto = new UpdateCategoryRequestDto(); + dto.name = 'UpdatedTestCategory'; + + await request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/categories/${categoryId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(200); + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories/search`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + categoryName: 'UpdatedTestCategory', + page: 1, + limit: 10, + }) + .expect(201) + .then(({ body }: { body: GetAllCategoriesResponseDto }) => { + expect(body.items.length).toBeGreaterThan(0); + expect(body.items[0].name).toBe('UpdatedTestCategory'); + }); + }); + + it('should return 404 for non-existent category', async () => { + const dto = new UpdateCategoryRequestDto(); + dto.name = 'UpdatedCategory'; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/categories/999`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(404); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new UpdateCategoryRequestDto(); + dto.name = 'UpdatedCategory'; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/categories/${categoryId}`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/categories/:categoryId (DELETE)', () => { + let categoryId: number; + + beforeEach(async () => { + const dto = new CreateCategoryRequestDto(); + dto.name = 'TestCategoryForDelete'; + + const response = await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + + categoryId = (response.body as { id: number }).id; + }); + + it('should delete category', async () => { + await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/categories/${categoryId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/categories/search`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + categoryName: 'TestCategoryForDelete', + page: 1, + limit: 10, + }) + .expect(201) + .then(({ body }: { body: GetAllCategoriesResponseDto }) => { + expect(body.items.length).toBe(0); + }); + }); + + it('should return 404 when deleting non-existent category', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/categories/999`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/categories/${categoryId}`) + .expect(401); + }); + }); + + afterAll(async () => { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + await delay(500); + await app.close(); + }); +}); diff --git a/apps/api/integration-test/test-specs/channel.integration-spec.ts b/apps/api/integration-test/test-specs/channel.integration-spec.ts index 6fe429ef6..cc181393a 100644 --- a/apps/api/integration-test/test-specs/channel.integration-spec.ts +++ b/apps/api/integration-test/test-specs/channel.integration-spec.ts @@ -243,6 +243,45 @@ describe('ChannelController (integration)', () => { expect(body.items.length).toBe(0); }); }); + + it('should return 401 when unauthorized', async () => { + await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/channels/1`) + .expect(401); + }); + }); + + describe('Channel validation tests', () => { + it('should return 400 when creating channel with invalid field key', async () => { + const dto = new CreateChannelRequestDto(); + dto.name = 'TestChannel'; + + const fieldDto = new CreateChannelRequestFieldDto(); + fieldDto.name = 'TestField'; + fieldDto.key = 'invalid-key!@#'; + fieldDto.format = FieldFormatEnum.text; + fieldDto.property = FieldPropertyEnum.EDITABLE; + fieldDto.status = FieldStatusEnum.ACTIVE; + + dto.fields = [fieldDto]; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/channels`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 400 when updating channel with invalid data', async () => { + const dto = new UpdateChannelRequestDto(); + dto.name = ''; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/channels/1`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); }); afterAll(async () => { diff --git a/apps/api/integration-test/test-specs/issue.integration-spec.ts b/apps/api/integration-test/test-specs/issue.integration-spec.ts index 0b39975d0..98a10bd86 100644 --- a/apps/api/integration-test/test-specs/issue.integration-spec.ts +++ b/apps/api/integration-test/test-specs/issue.integration-spec.ts @@ -210,7 +210,7 @@ describe('IssueController (integration)', () => { }); }); - describe('/admin/projects/:projectId/issues/:issueId (DELETE)', () => { + describe('/admin/projects/:projectId/issues (DELETE)', () => { it('should delete many issues', async () => { await request(app.getHttpServer() as Server) .delete(`/admin/projects/${project.id}/issues`) @@ -236,6 +236,41 @@ describe('IssueController (integration)', () => { expect(body.items.length).toBe(0); }); }); + + it('should return 200 when deleting with invalid issueIds', async () => { + await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/issues`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ issueIds: [] }) + .expect(200); + }); + + it('should return 401 when unauthorized', async () => { + await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/issues`) + .send({ issueIds: [1] }) + .expect(401); + }); + }); + + describe('Issue validation tests', () => { + it('should return 400 when updating non-existent issue', async () => { + await request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/issues/999`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + name: 'NonExistentIssue', + description: 'This should fail', + }) + .expect(400); + }); + + it('should return 400 when getting non-existent issue', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/issues/999`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(400); + }); }); afterAll(async () => { diff --git a/apps/api/integration-test/test-specs/member.integration-spec.ts b/apps/api/integration-test/test-specs/member.integration-spec.ts new file mode 100644 index 000000000..ded90ac57 --- /dev/null +++ b/apps/api/integration-test/test-specs/member.integration-spec.ts @@ -0,0 +1,427 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Server } from 'net'; +import { faker } from '@faker-js/faker'; +import type { INestApplication } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import request from 'supertest'; +import type { DataSource } from 'typeorm'; +import { initializeTransactionalContext } from 'typeorm-transactional'; + +import { AppModule } from '@/app.module'; +import { OpensearchRepository } from '@/common/repositories'; +import { AuthService } from '@/domains/admin/auth/auth.service'; +import { + CreateMemberRequestDto, + UpdateMemberRequestDto, +} from '@/domains/admin/project/member/dtos/requests'; +import type { GetAllMemberResponseDto } from '@/domains/admin/project/member/dtos/responses'; +import type { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { ProjectService } from '@/domains/admin/project/project/project.service'; +import { PermissionEnum } from '@/domains/admin/project/role/permission.enum'; +import type { RoleEntity } from '@/domains/admin/project/role/role.entity'; +import { RoleService } from '@/domains/admin/project/role/role.service'; +import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests'; +import { TenantService } from '@/domains/admin/tenant/tenant.service'; +import { + UserStateEnum, + UserTypeEnum, +} from '@/domains/admin/user/entities/enums'; +import { UserEntity } from '@/domains/admin/user/entities/user.entity'; +import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions'; + +describe('MemberController (integration)', () => { + let app: INestApplication; + + let dataSource: DataSource; + let authService: AuthService; + let tenantService: TenantService; + let projectService: ProjectService; + let roleService: RoleService; + + let configService: ConfigService; + let opensearchRepository: OpensearchRepository; + + let project: ProjectEntity; + let role: RoleEntity; + let user: UserEntity; + let accessToken: string; + + beforeAll(async () => { + initializeTransactionalContext(); + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + dataSource = module.get(getDataSourceToken()); + authService = module.get(AuthService); + tenantService = module.get(TenantService); + projectService = module.get(ProjectService); + roleService = module.get(RoleService); + configService = module.get(ConfigService); + opensearchRepository = module.get(OpensearchRepository); + + await clearAllEntities(module); + if (configService.get('opensearch.use')) { + await opensearchRepository.deleteAllIndexes(); + } + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.sample(); + dto.password = '12345678'; + await tenantService.create(dto); + + project = await projectService.create({ + name: faker.lorem.words(), + description: faker.lorem.lines(1), + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + }); + + role = await roleService.create({ + projectId: project.id, + name: 'TestRole', + permissions: [ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + ], + }); + + const { jwt } = await signInTestUser(dataSource, authService); + accessToken = jwt.accessToken; + + const userRepo = dataSource.getRepository(UserEntity); + user = await userRepo.save({ + email: faker.internet.email(), + state: UserStateEnum.Active, + hashPassword: faker.internet.password(), + type: UserTypeEnum.GENERAL, + }); + }); + + describe('/admin/projects/:projectId/members (POST)', () => { + afterEach(async () => { + await dataSource.query('DELETE FROM members WHERE role_id = ?', [ + role.id, + ]); + }); + + it('should create a member', async () => { + const dto = new CreateMemberRequestDto(); + dto.userId = user.id; + dto.roleId = role.id; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + }); + + it('should return 400 for duplicate member', async () => { + const dto = new CreateMemberRequestDto(); + dto.userId = user.id; + dto.roleId = role.id; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 400 for non-existent user', async () => { + const dto = new CreateMemberRequestDto(); + dto.userId = 999; + dto.roleId = role.id; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 404 for non-existent role', async () => { + const dto = new CreateMemberRequestDto(); + dto.userId = user.id; + dto.roleId = 999; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(404); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new CreateMemberRequestDto(); + dto.userId = user.id; + dto.roleId = role.id; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/members/search (POST)', () => { + afterEach(async () => { + await dataSource.query('DELETE FROM members WHERE role_id = ?', [ + role.id, + ]); + }); + + it('should find members by project id', async () => { + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + userId: user.id, + roleId: role.id, + }) + .expect(201); + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members/search`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + queries: [ + { + key: 'email', + value: user.email, + condition: 'LIKE', + }, + ], + operator: 'AND', + limit: 10, + page: 1, + }) + .expect(201) + .then(({ body }: { body: GetAllMemberResponseDto }) => { + const responseBody = body; + expect(responseBody.items.length).toBeGreaterThan(0); + expect(responseBody.items[0]).toHaveProperty('id'); + expect(responseBody.items[0]).toHaveProperty('user'); + expect(responseBody.items[0]).toHaveProperty('role'); + expect(responseBody.items[0].user).toHaveProperty('email'); + expect(responseBody.items[0].role).toHaveProperty('name'); + }); + }); + + it('should return empty list when no members match search', async () => { + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members/search`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + queries: [ + { + key: 'email', + value: 'NonExistentUser', + condition: 'LIKE', + }, + ], + operator: 'AND', + limit: 10, + page: 1, + }) + .expect(201) + .then(({ body }: { body: GetAllMemberResponseDto }) => { + const responseBody = body; + expect(responseBody.items.length).toBe(0); + }); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members/search`) + .send({ + queries: [], + operator: 'AND', + limit: 10, + page: 1, + }) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/members/:memberId (GET)', () => { + afterEach(async () => { + await dataSource.query('DELETE FROM members WHERE role_id = ?', [ + role.id, + ]); + }); + + it('should return 404 for non-existent member', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/members/999`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); + + describe('/admin/projects/:projectId/members/:memberId (PUT)', () => { + let memberId: number; + let newRole: RoleEntity; + + beforeEach(async () => { + newRole = await roleService.create({ + projectId: project.id, + name: `NewTestRole_${Date.now()}`, + permissions: [PermissionEnum.feedback_download_read], + }); + + const dto = new CreateMemberRequestDto(); + dto.userId = user.id; + dto.roleId = role.id; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + + const allMembers: { id: number }[] = await dataSource.query( + 'SELECT id FROM members ORDER BY id DESC LIMIT 1', + ); + memberId = allMembers.length > 0 ? allMembers[0].id : 1; + }); + + afterEach(async () => { + await dataSource.query( + 'DELETE FROM members WHERE role_id = ? OR role_id = ?', + [role.id, newRole.id], + ); + await dataSource.query('DELETE FROM roles WHERE id = ?', [newRole.id]); + }); + + it('should update member role', async () => { + const dto = new UpdateMemberRequestDto(); + dto.roleId = newRole.id; + + const response = await request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/members/${memberId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + + expect(response.status).toBe(200); + }); + + it('should return 404 for non-existent role', async () => { + const dto = new UpdateMemberRequestDto(); + dto.roleId = 999; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/members/${memberId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(404); + }); + + it('should return 400 for non-existent member', async () => { + const dto = new UpdateMemberRequestDto(); + dto.roleId = newRole.id; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/members/999`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new UpdateMemberRequestDto(); + dto.roleId = newRole.id; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/members/${memberId}`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/members/:memberId (DELETE)', () => { + let memberId: number; + + beforeEach(async () => { + const dto = new CreateMemberRequestDto(); + dto.userId = user.id; + dto.roleId = role.id; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/members`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + + const allMembers: { id: number }[] = await dataSource.query( + 'SELECT id FROM members ORDER BY id DESC LIMIT 1', + ); + memberId = allMembers.length > 0 ? allMembers[0].id : 1; + }); + + afterEach(async () => { + await dataSource.query('DELETE FROM members WHERE role_id = ?', [ + role.id, + ]); + }); + + it('should delete member', async () => { + const response = await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/members/${memberId}`) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toBe(200); + }); + + it('should return 200 when deleting non-existent member', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/members/999`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/members/${memberId}`) + .expect(401); + }); + }); + + afterAll(async () => { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + await delay(500); + await app.close(); + }); +}); diff --git a/apps/api/integration-test/test-specs/role.integration-spec.ts b/apps/api/integration-test/test-specs/role.integration-spec.ts new file mode 100644 index 000000000..7f4f71026 --- /dev/null +++ b/apps/api/integration-test/test-specs/role.integration-spec.ts @@ -0,0 +1,357 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Server } from 'net'; +import { faker } from '@faker-js/faker'; +import type { INestApplication } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import request from 'supertest'; +import type { DataSource } from 'typeorm'; +import { initializeTransactionalContext } from 'typeorm-transactional'; + +import { AppModule } from '@/app.module'; +import { OpensearchRepository } from '@/common/repositories'; +import { AuthService } from '@/domains/admin/auth/auth.service'; +import type { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { ProjectService } from '@/domains/admin/project/project/project.service'; +import { + CreateRoleRequestDto, + UpdateRoleRequestDto, +} from '@/domains/admin/project/role/dtos/requests'; +import type { GetAllRolesResponseDto } from '@/domains/admin/project/role/dtos/responses'; +import type { GetAllRolesResponseRoleDto } from '@/domains/admin/project/role/dtos/responses/get-all-roles-response.dto'; +import { PermissionEnum } from '@/domains/admin/project/role/permission.enum'; +import { RoleService } from '@/domains/admin/project/role/role.service'; +import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests'; +import { TenantService } from '@/domains/admin/tenant/tenant.service'; +import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions'; + +describe('RoleController (integration)', () => { + let app: INestApplication; + + let dataSource: DataSource; + let authService: AuthService; + let tenantService: TenantService; + let projectService: ProjectService; + let _roleService: RoleService; + let configService: ConfigService; + let opensearchRepository: OpensearchRepository; + + let project: ProjectEntity; + let accessToken: string; + + beforeAll(async () => { + initializeTransactionalContext(); + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + dataSource = module.get(getDataSourceToken()); + authService = module.get(AuthService); + tenantService = module.get(TenantService); + projectService = module.get(ProjectService); + _roleService = module.get(RoleService); + configService = module.get(ConfigService); + opensearchRepository = module.get(OpensearchRepository); + + await clearAllEntities(module); + if (configService.get('opensearch.use')) { + await opensearchRepository.deleteAllIndexes(); + } + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.sample(); + dto.password = '12345678'; + await tenantService.create(dto); + + project = await projectService.create({ + name: faker.lorem.words(), + description: faker.lorem.lines(1), + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + }); + + const { jwt } = await signInTestUser(dataSource, authService); + accessToken = jwt.accessToken; + }); + + describe('/admin/projects/:projectId/roles (POST)', () => { + it('should create a role', async () => { + const dto = new CreateRoleRequestDto(); + dto.name = 'TestRole'; + dto.permissions = [ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + ]; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + + const listResponse = await request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .query({ + searchText: 'TestRole', + page: 1, + limit: 10, + }) + .expect(200); + + const roles = (listResponse.body as GetAllRolesResponseDto).roles; + expect(roles.length).toBeGreaterThan(0); + + const createdRole = roles.find( + (role: GetAllRolesResponseRoleDto) => role.name === 'TestRole', + ); + expect(createdRole).toBeDefined(); + expect(createdRole?.name).toBe('TestRole'); + expect(createdRole?.permissions).toEqual([ + 'feedback_download_read', + 'feedback_update', + ]); + }); + + it('should return 400 for empty role name', async () => { + const dto = new CreateRoleRequestDto(); + dto.permissions = [PermissionEnum.feedback_download_read]; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 400 for invalid permissions', async () => { + const dto = new CreateRoleRequestDto(); + dto.name = 'TestRole'; + dto.permissions = []; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new CreateRoleRequestDto(); + dto.name = 'TestRole'; + dto.permissions = [PermissionEnum.feedback_download_read]; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/roles`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/roles (GET)', () => { + beforeEach(async () => { + const dto = new CreateRoleRequestDto(); + dto.name = 'TestRoleForList'; + dto.permissions = [PermissionEnum.feedback_download_read]; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + }); + + it('should find roles by project id', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .query({ + searchText: 'TestRole', + page: 1, + limit: 10, + }) + .expect(200) + .then(({ body }: { body: GetAllRolesResponseDto }) => { + const responseBody = body; + expect(responseBody.roles.length).toBeGreaterThan(0); + expect(responseBody.roles[0]).toHaveProperty('id'); + expect(responseBody.roles[0]).toHaveProperty('name'); + expect(responseBody.roles[0]).toHaveProperty('permissions'); + }); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/roles`) + .query({ + page: 1, + limit: 10, + }) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/roles/:roleId (PUT)', () => { + let roleId: number; + + beforeAll(async () => { + const dto = new CreateRoleRequestDto(); + dto.name = 'TestRoleForUpdate'; + dto.permissions = [PermissionEnum.feedback_download_read]; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + + const response = await request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const roles = (response.body as GetAllRolesResponseDto).roles; + const createdRole = roles.find( + (role: GetAllRolesResponseRoleDto) => role.name === 'TestRoleForUpdate', + ); + if (!createdRole) { + throw new Error('TestRoleForUpdate not found'); + } + roleId = createdRole.id; + }); + + it('should update role', async () => { + const dto = new UpdateRoleRequestDto(); + dto.name = 'UpdatedTestRole'; + dto.permissions = [PermissionEnum.feedback_download_read]; + + await request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/roles/${roleId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(204); + + await request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .then(({ body }: { body: GetAllRolesResponseDto }) => { + const roles = body.roles; + const updatedRole = roles.find( + (role: GetAllRolesResponseRoleDto) => + role.name === 'UpdatedTestRole', + ); + if (!updatedRole) { + throw new Error('UpdatedTestRole not found'); + } + expect(updatedRole.name).toBe('UpdatedTestRole'); + expect(updatedRole.permissions).toEqual(['feedback_download_read']); + }); + }); + + it('should return 400 for empty role name', async () => { + const dto = new UpdateRoleRequestDto(); + dto.permissions = [PermissionEnum.feedback_download_read]; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/roles/${roleId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new UpdateRoleRequestDto(); + dto.name = 'UpdatedRole'; + dto.permissions = [PermissionEnum.feedback_download_read]; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/roles/${roleId}`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/roles/:roleId (DELETE)', () => { + let roleId: number; + + beforeAll(async () => { + const dto = new CreateRoleRequestDto(); + dto.name = 'TestRoleForDelete'; + dto.permissions = [PermissionEnum.feedback_download_read]; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + + const response = await request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const roles = (response.body as GetAllRolesResponseDto).roles; + const createdRole = roles.find( + (role: GetAllRolesResponseRoleDto) => role.name === 'TestRoleForDelete', + ); + if (!createdRole) { + throw new Error('TestRoleForDelete not found'); + } + roleId = createdRole.id; + }); + + it('should delete role', async () => { + await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/roles/${roleId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const response = await request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/roles`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const roles = (response.body as GetAllRolesResponseDto).roles; + const deletedRole = roles.find( + (role: GetAllRolesResponseRoleDto) => role.name === 'TestRoleForDelete', + ); + expect(deletedRole).toBeUndefined(); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/roles/${roleId}`) + .expect(401); + }); + }); + + afterAll(async () => { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + await delay(500); + await app.close(); + }); +}); diff --git a/apps/api/integration-test/test-specs/webhook.integration-spec.ts b/apps/api/integration-test/test-specs/webhook.integration-spec.ts new file mode 100644 index 000000000..37a7a2902 --- /dev/null +++ b/apps/api/integration-test/test-specs/webhook.integration-spec.ts @@ -0,0 +1,486 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Server } from 'net'; +import { faker } from '@faker-js/faker'; +import type { INestApplication } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { TestingModule } from '@nestjs/testing'; +import { Test } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import request from 'supertest'; +import type { DataSource } from 'typeorm'; +import { initializeTransactionalContext } from 'typeorm-transactional'; + +import { AppModule } from '@/app.module'; +import { + EventStatusEnum, + EventTypeEnum, + WebhookStatusEnum, +} from '@/common/enums'; +import { OpensearchRepository } from '@/common/repositories'; +import { AuthService } from '@/domains/admin/auth/auth.service'; +import type { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { ProjectService } from '@/domains/admin/project/project/project.service'; +import { + CreateWebhookRequestDto, + UpdateWebhookRequestDto, +} from '@/domains/admin/project/webhook/dtos/requests'; +import type { + GetWebhookByIdResponseDto, + GetWebhooksByProjectIdResponseDto, +} from '@/domains/admin/project/webhook/dtos/responses'; +import { SetupTenantRequestDto } from '@/domains/admin/tenant/dtos/requests'; +import { TenantService } from '@/domains/admin/tenant/tenant.service'; +import { clearAllEntities, signInTestUser } from '@/test-utils/util-functions'; + +describe('WebhookController (integration)', () => { + let app: INestApplication; + + let dataSource: DataSource; + let authService: AuthService; + let tenantService: TenantService; + let projectService: ProjectService; + let configService: ConfigService; + let opensearchRepository: OpensearchRepository; + + let project: ProjectEntity; + let accessToken: string; + + beforeAll(async () => { + initializeTransactionalContext(); + const module: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + + dataSource = module.get(getDataSourceToken()); + authService = module.get(AuthService); + tenantService = module.get(TenantService); + projectService = module.get(ProjectService); + configService = module.get(ConfigService); + opensearchRepository = module.get(OpensearchRepository); + + await clearAllEntities(module); + if (configService.get('opensearch.use')) { + await opensearchRepository.deleteAllIndexes(); + } + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.sample(); + dto.password = '12345678'; + await tenantService.create(dto); + + project = await projectService.create({ + name: faker.lorem.words(), + description: faker.lorem.lines(1), + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + }); + + const { jwt } = await signInTestUser(dataSource, authService); + accessToken = jwt.accessToken; + }); + + describe('/admin/projects/:projectId/webhooks (POST)', () => { + it('should create a webhook', async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhook'; + dto.url = 'https://example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(201); + + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(200) + .then(({ body }: { body: GetWebhooksByProjectIdResponseDto }) => { + expect(body.items[0].name).toBe('TestWebhook'); + expect(body.items[0].url).toBe('https://example.com/webhook'); + expect(body.items[0].events).toHaveLength(1); + expect(body.items[0].status).toBe(WebhookStatusEnum.ACTIVE); + expect(body.items[0].createdAt).toBeDefined(); + }); + }); + + it('should return 400 for empty webhook name', async () => { + const dto = new CreateWebhookRequestDto(); + dto.url = 'https://example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 400 for invalid URL format', async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhook'; + dto.url = 'invalid-url'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 400 for empty events array', async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhook'; + dto.url = 'https://example.com/webhook'; + dto.events = []; + dto.status = WebhookStatusEnum.ACTIVE; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(400); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhook'; + dto.url = 'https://example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + return request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/webhooks (GET)', () => { + beforeEach(async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhookForList'; + dto.url = 'https://example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + }); + + it('should find webhooks by project id', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .query({ + searchText: 'TestWebhook', + page: 1, + limit: 10, + }) + .expect(200) + .then(({ body }: { body: GetWebhooksByProjectIdResponseDto }) => { + const responseBody = body; + expect(responseBody.items.length).toBeGreaterThan(0); + expect(responseBody.items[0]).toHaveProperty('id'); + expect(responseBody.items[0]).toHaveProperty('name'); + expect(responseBody.items[0]).toHaveProperty('url'); + expect(responseBody.items[0]).toHaveProperty('events'); + expect(responseBody.items[0]).toHaveProperty('status'); + expect(responseBody.items[0]).toHaveProperty('createdAt'); + }); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/webhooks`) + .query({ + page: 1, + limit: 10, + }) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/webhooks/:webhookId (GET)', () => { + let webhookId: number; + + beforeEach(async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhookForGet'; + dto.url = 'https://example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + const response = await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + + webhookId = (response.body as { id: number }).id; + }); + + it('should find webhook by id', async () => { + const response = await request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + const body = response.body as GetWebhookByIdResponseDto[]; + expect(response.body).toBeDefined(); + expect(body[0].id).toBe(webhookId); + expect(body[0].name).toBe('TestWebhookForGet'); + expect(body[0].url).toBe('https://example.com/webhook'); + expect(body[0].events).toHaveLength(1); + expect(body[0].status).toBe(WebhookStatusEnum.ACTIVE); + expect(body[0].createdAt).toBeDefined(); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/webhooks/:webhookId (PUT)', () => { + let webhookId: number; + + beforeEach(async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhookForUpdate'; + dto.url = 'https://example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + const response = await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + + webhookId = (response.body as { id: number }).id; + }); + + it('should update webhook', async () => { + const dto = new UpdateWebhookRequestDto(); + dto.name = 'UpdatedTestWebhook'; + dto.url = 'https://updated-example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + dto.token = null; + + await request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(200); + + const response = await request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + expect(response.body).toBeDefined(); + const body = response.body as GetWebhookByIdResponseDto[]; + expect(body[0].name).toBe('UpdatedTestWebhook'); + expect(body[0].url).toBe('https://updated-example.com/webhook'); + expect(body[0].events).toHaveLength(1); + expect(body[0].events[0].type).toBe(EventTypeEnum.FEEDBACK_CREATION); + expect(body[0].status).toBe(WebhookStatusEnum.ACTIVE); + }); + + it('should update webhook with empty name', async () => { + const dto = new UpdateWebhookRequestDto(); + dto.name = ''; + dto.url = 'https://updated-example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + dto.token = null; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(200); + }); + + it('should return 404 for non-existent webhook', async () => { + const dto = new UpdateWebhookRequestDto(); + dto.name = 'UpdatedWebhook'; + dto.url = 'https://updated-example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + dto.token = null; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/webhooks/999`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto) + .expect(404); + }); + + it('should return 401 when unauthorized', async () => { + const dto = new UpdateWebhookRequestDto(); + dto.name = 'UpdatedWebhook'; + dto.url = 'https://updated-example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + return request(app.getHttpServer() as Server) + .put(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .send(dto) + .expect(401); + }); + }); + + describe('/admin/projects/:projectId/webhooks/:webhookId (DELETE)', () => { + let webhookId: number; + + beforeEach(async () => { + const dto = new CreateWebhookRequestDto(); + dto.name = 'TestWebhookForDelete'; + dto.url = 'https://example.com/webhook'; + dto.events = [ + { + type: EventTypeEnum.FEEDBACK_CREATION, + status: EventStatusEnum.ACTIVE, + channelIds: [], + }, + ]; + dto.status = WebhookStatusEnum.ACTIVE; + + const response = await request(app.getHttpServer() as Server) + .post(`/admin/projects/${project.id}/webhooks`) + .set('Authorization', `Bearer ${accessToken}`) + .send(dto); + + webhookId = (response.body as { id: number }).id; + }); + + it('should delete webhook', async () => { + await request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + + return request(app.getHttpServer() as Server) + .get(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200); + }); + + it('should return 404 when deleting non-existent webhook', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/webhooks/999`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + + it('should return 401 when unauthorized', async () => { + return request(app.getHttpServer() as Server) + .delete(`/admin/projects/${project.id}/webhooks/${webhookId}`) + .expect(401); + }); + }); + + afterAll(async () => { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + await delay(500); + await app.close(); + }); +}); diff --git a/apps/api/jest.config.js b/apps/api/jest.config.js index 8f8dc8f03..3a9100c32 100644 --- a/apps/api/jest.config.js +++ b/apps/api/jest.config.js @@ -1,15 +1,16 @@ -export default { +module.exports = { displayName: 'api', rootDir: './src', testRegex: '.*\\.spec\\.ts$', collectCoverageFrom: ['**/*.(t|j)s'], testEnvironment: 'node', moduleNameMapper: { - '^@/(.*)$': ['/$2'], + '^@/(.*)$': ['/$1'], }, transform: { '^.+\\.(t|j)s$': ['@swc-node/jest'], }, + transformIgnorePatterns: ['node_modules/(?!@faker-js|uuid)'], moduleFileExtensions: ['js', 'json', 'ts'], coverageDirectory: '../coverage', clearMocks: true, diff --git a/apps/api/package.json b/apps/api/package.json index cafbe0b86..635b7a4e0 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -26,22 +26,22 @@ }, "prettier": "@ufb/prettier-config", "dependencies": { - "@aws-sdk/client-s3": "^3.884.0", - "@aws-sdk/s3-request-presigner": "^3.884.0", - "@fastify/multipart": "^9.2.1", - "@fastify/static": "^8.2.0", + "@aws-sdk/client-s3": "^3.971.0", + "@aws-sdk/s3-request-presigner": "^3.971.0", + "@fastify/multipart": "^9.4.0", + "@fastify/static": "^9.0.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^4.0.1", - "@nestjs/common": "^11.1.6", + "@nestjs/common": "^11.1.12", "@nestjs/config": "^4.0.2", - "@nestjs/core": "^11.1.6", + "@nestjs/core": "^11.1.12", "@nestjs/event-emitter": "^3.0.1", - "@nestjs/jwt": "^11.0.0", + "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", - "@nestjs/platform-express": "^11.1.6", - "@nestjs/platform-fastify": "^11.1.6", - "@nestjs/schedule": "^6.0.0", - "@nestjs/swagger": "^11.2.0", + "@nestjs/platform-express": "^11.1.12", + "@nestjs/platform-fastify": "^11.1.12", + "@nestjs/schedule": "^6.0.1", + "@nestjs/swagger": "^11.2.5", "@nestjs/terminus": "^11.0.0", "@nestjs/typeorm": "^11.0.0", "@opensearch-project/opensearch": "^3.5.1", @@ -49,65 +49,65 @@ "@types/passport-local": "^1.0.38", "@ufb/shared": "workspace:*", "@willsoto/nestjs-prometheus": "^6.0.2", - "axios": "^1.11.0", + "axios": "^1.13.2", "bcrypt": "^6.0.0", "class-transformer": "^0.5.1", - "class-validator": "^0.14.2", - "cron": "^4.3.0", - "dotenv": "^17.2.2", + "class-validator": "^0.14.3", + "cron": "^4.3.3", + "dotenv": "^17.2.3", "exceljs": "^4.4.0", "fast-csv": "^5.0.5", - "fastify": "^5.4.0", - "joi": "^18.0.1", + "fastify": "^5.7.1", + "joi": "^18.0.2", "luxon": "^3.7.2", "magic-bytes.js": "^1.12.1", - "mysql2": "^3.14.5", - "nestjs-cls": "^6.0.1", - "nestjs-pino": "^4.4.0", + "mysql2": "^3.16.1", + "nestjs-cls": "^6.2.0", + "nestjs-pino": "^4.5.0", "nestjs-typeorm-paginate": "^4.1.0", - "nodemailer": "^7.0.6", + "nodemailer": "^7.0.12", "passport": "^0.7.0", "passport-custom": "^1.1.1", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", - "pino-http": "^10.5.0", - "pino-pretty": "^13.1.1", + "pino-http": "^11.0.0", + "pino-pretty": "^13.1.3", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "source-map-support": "^0.5.21", - "typeorm": "^0.3.26", + "typeorm": "^0.3.28", "typeorm-naming-strategies": "^4.1.0", "typeorm-transactional": "^0.5.0", - "uuid": "^11.1.0" + "uuid": "^13.0.0" }, "devDependencies": { - "@faker-js/faker": "^9.9.0", - "@nestjs/cli": "^11.0.10", - "@nestjs/schematics": "^11.0.7", - "@nestjs/testing": "^11.1.6", + "@faker-js/faker": "^10.2.0", + "@nestjs/cli": "^11.0.16", + "@nestjs/schematics": "^11.0.9", + "@nestjs/testing": "^11.1.12", "@swc-node/jest": "^1.9.1", - "@swc/cli": "0.7.8", + "@swc/cli": "0.7.10", "@swc/core": "1.13.5", - "@swc/helpers": "^0.5.17", + "@swc/helpers": "^0.5.18", "@types/bcrypt": "^6.0.0", - "@types/express": "^5.0.3", + "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/luxon": "^3.7.1", - "@types/node": "22.18.1", - "@types/nodemailer": "^7.0.1", + "@types/node": "24.10.8", + "@types/nodemailer": "^7.0.5", "@types/passport-jwt": "*", "@types/supertest": "^6.0.3", - "@typescript-eslint/parser": "^8.43.0", + "@typescript-eslint/parser": "^8.46.0", "@ufb/eslint-config": "workspace:*", "@ufb/prettier-config": "workspace:*", "@ufb/tsconfig": "workspace:*", "eslint": "catalog:", - "jest": "^30.1.3", + "jest": "^30.2.0", "mockdate": "^3.0.5", "prettier": "catalog:", - "supertest": "^7.1.4", - "ts-jest": "^29.4.1", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", diff --git a/apps/api/src/common/filters/http-exception.filter.spec.ts b/apps/api/src/common/filters/http-exception.filter.spec.ts new file mode 100644 index 000000000..83939785f --- /dev/null +++ b/apps/api/src/common/filters/http-exception.filter.spec.ts @@ -0,0 +1,300 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { ArgumentsHost } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import type { FastifyReply, FastifyRequest } from 'fastify'; + +import { HttpExceptionFilter } from './http-exception.filter'; + +describe('HttpExceptionFilter', () => { + let filter: HttpExceptionFilter; + let mockRequest: FastifyRequest; + let mockResponse: FastifyReply; + let mockArgumentsHost: ArgumentsHost; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [HttpExceptionFilter], + }).compile(); + + filter = module.get(HttpExceptionFilter); + + // Mock FastifyRequest + mockRequest = { + url: '/test-endpoint', + method: 'GET', + headers: {}, + query: {}, + params: {}, + body: {}, + } as FastifyRequest; + + // Mock FastifyReply + mockResponse = { + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + } as unknown as FastifyReply; + + // Mock ArgumentsHost + mockArgumentsHost = { + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue(mockRequest), + getResponse: jest.fn().mockReturnValue(mockResponse), + }), + } as unknown as ArgumentsHost; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('catch', () => { + it('should handle string exception response', () => { + const exception = new HttpException( + 'Test error message', + HttpStatus.BAD_REQUEST, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(mockResponse.send).toHaveBeenCalledWith({ + response: 'Test error message', + path: '/test-endpoint', + }); + }); + + it('should handle object exception response', () => { + const exceptionResponse = { + message: 'Validation failed', + error: 'Bad Request', + statusCode: HttpStatus.BAD_REQUEST, + }; + const exception = new HttpException( + exceptionResponse, + HttpStatus.BAD_REQUEST, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(mockResponse.send).toHaveBeenCalledWith({ + message: 'Validation failed', + error: 'Bad Request', + statusCode: HttpStatus.BAD_REQUEST, + path: '/test-endpoint', + }); + }); + + it('should handle different HTTP status codes', () => { + const statusCodes = [ + HttpStatus.UNAUTHORIZED, + HttpStatus.FORBIDDEN, + HttpStatus.NOT_FOUND, + HttpStatus.INTERNAL_SERVER_ERROR, + ]; + + statusCodes.forEach((statusCode) => { + const exception = new HttpException('Test error', statusCode); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(statusCode); + expect(mockResponse.send).toHaveBeenCalledWith({ + response: 'Test error', + path: '/test-endpoint', + }); + }); + }); + + it('should handle complex object exception response', () => { + const exceptionResponse = { + message: ['Email is required', 'Password is too short'], + error: 'Validation Error', + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + details: { + field: 'email', + value: '', + }, + }; + const exception = new HttpException( + exceptionResponse, + HttpStatus.UNPROCESSABLE_ENTITY, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith( + HttpStatus.UNPROCESSABLE_ENTITY, + ); + expect(mockResponse.send).toHaveBeenCalledWith({ + message: ['Email is required', 'Password is too short'], + error: 'Validation Error', + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + path: '/test-endpoint', + details: { + field: 'email', + value: '', + }, + }); + }); + + it('should handle empty string exception response', () => { + const exception = new HttpException('', HttpStatus.NO_CONTENT); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT); + expect(mockResponse.send).toHaveBeenCalledWith({ + response: '', + path: '/test-endpoint', + }); + }); + + it('should handle null exception response', () => { + const exception = new HttpException(null as any, HttpStatus.NO_CONTENT); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT); + expect(mockResponse.send).toHaveBeenCalledWith({ + statusCode: HttpStatus.NO_CONTENT, + path: '/test-endpoint', + }); + }); + + it('should handle undefined exception response', () => { + const exception = new HttpException( + undefined as any, + HttpStatus.NO_CONTENT, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NO_CONTENT); + expect(mockResponse.send).toHaveBeenCalledWith({ + statusCode: HttpStatus.NO_CONTENT, + path: '/test-endpoint', + }); + }); + + it('should handle different request URLs', () => { + const urls = [ + '/api/users', + '/api/projects/123', + '/api/auth/login', + '/api/feedback?page=1&limit=10', + ]; + + urls.forEach((url) => { + Object.assign(mockRequest, { url }); + const exception = new HttpException( + 'Test error', + HttpStatus.BAD_REQUEST, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.send).toHaveBeenCalledWith({ + response: 'Test error', + path: url, + }); + }); + }); + + it('should handle nested object exception response', () => { + const exceptionResponse = { + message: 'Complex error', + error: 'Internal Server Error', + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + nested: { + level1: { + level2: { + value: 'deep nested value', + }, + }, + }, + }; + const exception = new HttpException( + exceptionResponse, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith( + HttpStatus.INTERNAL_SERVER_ERROR, + ); + expect(mockResponse.send).toHaveBeenCalledWith({ + message: 'Complex error', + error: 'Internal Server Error', + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + path: '/test-endpoint', + nested: { + level1: { + level2: { + value: 'deep nested value', + }, + }, + }, + }); + }); + + it('should handle array exception response', () => { + const exceptionResponse = ['Error 1', 'Error 2', 'Error 3']; + const exception = new HttpException( + exceptionResponse, + HttpStatus.BAD_REQUEST, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(mockResponse.send).toHaveBeenCalledWith({ + 0: 'Error 1', + 1: 'Error 2', + 2: 'Error 3', + statusCode: HttpStatus.BAD_REQUEST, + path: '/test-endpoint', + }); + }); + + it('should handle boolean exception response', () => { + const exception = new HttpException(true as any, HttpStatus.OK); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK); + expect(mockResponse.send).toHaveBeenCalledWith({ + statusCode: HttpStatus.OK, + path: '/test-endpoint', + }); + }); + + it('should handle number exception response', () => { + const exception = new HttpException(42 as any, HttpStatus.OK); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK); + expect(mockResponse.send).toHaveBeenCalledWith({ + statusCode: HttpStatus.OK, + path: '/test-endpoint', + }); + }); + }); +}); diff --git a/apps/api/src/common/repositories/opensearch.repository.spec.ts b/apps/api/src/common/repositories/opensearch.repository.spec.ts index f31d197de..25476635b 100644 --- a/apps/api/src/common/repositories/opensearch.repository.spec.ts +++ b/apps/api/src/common/repositories/opensearch.repository.spec.ts @@ -111,6 +111,35 @@ describe('Opensearch Repository Test suite', () => { name: index, }); }); + + it('creating index handles errors', async () => { + const index = faker.number.int().toString(); + const error = new Error('Index creation failed'); + + jest.spyOn(osClient.indices, 'create').mockRejectedValue(error as never); + + await expect(osRepo.createIndex({ index })).rejects.toThrow( + 'Index creation failed', + ); + }); + + it('creating index handles OpenSearch specific errors', async () => { + const index = faker.number.int().toString(); + const error = { + meta: { + body: { + error: { + type: 'resource_already_exists_exception', + reason: 'index already exists', + }, + }, + }, + }; + + jest.spyOn(osClient.indices, 'create').mockRejectedValue(error as never); + + await expect(osRepo.createIndex({ index })).rejects.toEqual(error); + }); }); describe('putMappings', () => { @@ -148,6 +177,33 @@ describe('Opensearch Repository Test suite', () => { expect(osClient.indices.exists).toHaveBeenCalledTimes(1); expect(osClient.indices.putMapping).not.toHaveBeenCalled(); }); + + it('putting mappings handles OpenSearch errors', async () => { + const dto = new PutMappingsDto(); + dto.index = faker.number.int().toString(); + dto.mappings = MAPPING_JSON; + + jest + .spyOn(osClient.indices, 'exists') + .mockResolvedValue({ statusCode: 200 } as never); + + const error = { + meta: { + body: { + error: { + type: 'illegal_argument_exception', + reason: 'mapping update failed', + }, + }, + }, + }; + + jest + .spyOn(osClient.indices, 'putMapping') + .mockRejectedValue(error as never); + + await expect(osRepo.putMappings(dto)).rejects.toEqual(error); + }); }); describe('createData', () => { @@ -261,26 +317,381 @@ describe('Opensearch Repository Test suite', () => { }); describe('getData', () => { - return; + it('getting data succeeds with valid inputs', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + const sort = ['_id:desc']; + const limit = 10; + const page = 1; + + jest.spyOn(osClient, 'search').mockResolvedValue({ + body: { + hits: { + hits: [ + { _source: { KEY1: 'VALUE1' } }, + { _source: { KEY2: 'VALUE2' } }, + ], + total: 2, + }, + }, + } as never); + + const result = await osRepo.getData({ index, query, sort, limit, page }); + + expect(result.items).toHaveLength(2); + expect(result.total).toBe(2); + expect(osClient.search).toHaveBeenCalledWith({ + index, + from: 0, + size: limit, + sort, + body: { query }, + }); + }); + + it('getting data with empty sort adds default sort', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + const sort: string[] = []; + + jest.spyOn(osClient, 'search').mockResolvedValue({ + body: { + hits: { + hits: [], + total: 0, + }, + }, + } as never); + + await osRepo.getData({ index, query, sort, page: 1, limit: 100 }); + + expect(osClient.search).toHaveBeenCalledWith({ + index, + from: 0, + size: 100, + sort: ['_id:desc'], + body: { query }, + }); + }); + + it('getting data handles large window exception', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + + const error = new Error('Result window is too large'); + error.name = 'OpenSearchClientError'; + + jest.spyOn(osClient, 'search').mockRejectedValue(error as never); + + await expect( + osRepo.getData({ index, query, sort: [], page: 1, limit: 100 }), + ).rejects.toThrow('Result window is too large'); + }); + + it('getting data handles total as object', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + + jest.spyOn(osClient, 'search').mockResolvedValue({ + body: { + hits: { + hits: [], + total: { value: 100, relation: 'eq' }, + }, + }, + } as never); + + const result = await osRepo.getData({ + index, + query, + sort: [], + page: 1, + limit: 100, + }); + + expect(result.total).toBe(100); + }); }); describe('scroll', () => { - return; + it('scrolling with scrollId succeeds', async () => { + const scrollId = faker.string.alphanumeric(32); + const mockData = [{ KEY1: 'VALUE1' }, { KEY2: 'VALUE2' }]; + + jest.spyOn(osClient, 'scroll').mockResolvedValue({ + body: { + hits: { + hits: mockData.map((data) => ({ _source: data })), + }, + _scroll_id: scrollId, + }, + } as never); + + const result = await osRepo.scroll({ + scrollId, + index: '', + size: 10, + query: { bool: { must: [{ term: { status: 'active' } }] } }, + sort: [], + }); + + expect(result.data).toEqual(mockData); + expect(result.scrollId).toEqual(scrollId); + expect(osClient.scroll).toHaveBeenCalledWith({ + scroll_id: scrollId, + scroll: '1m', + }); + }); + + it('scrolling without scrollId performs initial search', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + const sort = ['_id:desc']; + const size = 10; + const mockData = [{ KEY1: 'VALUE1' }]; + + jest.spyOn(osClient, 'search').mockResolvedValue({ + body: { + hits: { + hits: mockData.map((data) => ({ _source: data })), + }, + _scroll_id: 'new_scroll_id', + }, + } as never); + + const result = await osRepo.scroll({ + index, + query, + sort, + size, + scrollId: null, + }); + + expect(result.data).toEqual(mockData); + expect(result.scrollId).toEqual('new_scroll_id'); + expect(osClient.search).toHaveBeenCalledWith({ + index, + size, + sort, + body: { query }, + scroll: '1m', + }); + }); + + it('scrolling with empty sort adds default sort', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + const sort: string[] = []; + + jest.spyOn(osClient, 'search').mockResolvedValue({ + body: { + hits: { hits: [] }, + _scroll_id: 'scroll_id', + }, + } as never); + + await osRepo.scroll({ index, query, sort, size: 10, scrollId: null }); + + expect(osClient.search).toHaveBeenCalledWith({ + index, + size: 10, + sort: ['_id:desc'], + body: { query }, + scroll: '1m', + }); + }); }); describe('updateData', () => { - return; + it('updating data succeeds with valid inputs', async () => { + const index = faker.number.int().toString(); + const id = faker.number.int().toString(); + const updateData = { KEY1: 'UPDATED_VALUE' }; + + jest.spyOn(osClient, 'update').mockResolvedValue({ + body: { + _id: id, + result: 'updated', + }, + } as never); + + await osRepo.updateData({ index, id, data: updateData }); + + expect(osClient.update).toHaveBeenCalledWith({ + index, + id, + body: { + doc: updateData, + }, + refresh: true, + retry_on_conflict: 5, + }); + }); + + it('updating data handles errors', async () => { + const index = faker.number.int().toString(); + const id = faker.number.int().toString(); + const updateData = { KEY1: 'UPDATED_VALUE' }; + const error = new Error('Update failed'); + + jest.spyOn(osClient, 'update').mockRejectedValue(error as never); + + await expect( + osRepo.updateData({ index, id, data: updateData }), + ).rejects.toThrow('Update failed'); + }); }); describe('deleteBulkData', () => { - return; + it('deleting bulk data succeeds with valid ids', async () => { + const index = faker.number.int().toString(); + const ids = [faker.number.int(), faker.number.int()]; + + jest.spyOn(osClient, 'deleteByQuery').mockResolvedValue({ + body: { + deleted: ids.length, + }, + } as never); + + await osRepo.deleteBulkData({ index, ids }); + + expect(osClient.deleteByQuery).toHaveBeenCalledWith({ + index, + body: { query: { terms: { _id: ids } } }, + refresh: true, + }); + }); + + it('deleting bulk data with empty ids array', async () => { + const index = faker.number.int().toString(); + const ids: number[] = []; + + jest.spyOn(osClient, 'deleteByQuery').mockResolvedValue({ + body: { + deleted: 0, + }, + } as never); + + await osRepo.deleteBulkData({ index, ids }); + + expect(osClient.deleteByQuery).toHaveBeenCalledWith({ + index, + body: { query: { terms: { _id: ids } } }, + refresh: true, + }); + }); }); describe('deleteIndex', () => { - return; + it('deleting index succeeds with valid index', async () => { + const index = faker.number.int().toString(); + const indexName = 'channel_' + index; + + jest.spyOn(osClient.indices, 'delete').mockResolvedValue({ + body: { + acknowledged: true, + }, + } as never); + + await osRepo.deleteIndex(index); + + expect(osClient.indices.delete).toHaveBeenCalledWith({ + index: indexName, + }); + }); + + it('deleting index handles errors', async () => { + const index = faker.number.int().toString(); + const error = new Error('Delete failed'); + + jest.spyOn(osClient.indices, 'delete').mockRejectedValue(error as never); + + await expect(osRepo.deleteIndex(index)).rejects.toThrow('Delete failed'); + }); }); describe('getTotal', () => { - return; + it('getting total count succeeds with valid query', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + + jest.spyOn(osClient, 'count').mockResolvedValue({ + body: { + count: 100, + }, + } as never); + + const result = await osRepo.getTotal(index, query); + + expect(result).toBe(100); + expect(osClient.count).toHaveBeenCalledWith({ + index, + body: { query }, + }); + }); + + it('getting total count with complex query', async () => { + const index = faker.number.int().toString(); + const query = { + bool: { + must: [ + { term: { status: 'active' } }, + { range: { created_at: { gte: '2023-01-01' } } }, + ], + }, + }; + + jest.spyOn(osClient, 'count').mockResolvedValue({ + body: { + count: 50, + }, + } as never); + + const result = await osRepo.getTotal(index, query); + + expect(result).toBe(50); + expect(osClient.count).toHaveBeenCalledWith({ + index, + body: { query }, + }); + }); + + it('getting total count handles errors', async () => { + const index = faker.number.int().toString(); + const query = { bool: { must: [{ term: { status: 'active' } }] } }; + const error = new Error('Count failed'); + + jest.spyOn(osClient, 'count').mockRejectedValue(error as never); + + await expect(osRepo.getTotal(index, query)).rejects.toThrow( + 'Count failed', + ); + }); + }); + + describe('deleteAllIndexes', () => { + it('deleting all indexes succeeds', async () => { + jest.spyOn(osClient.indices, 'delete').mockResolvedValue({ + body: { + acknowledged: true, + }, + } as never); + + await osRepo.deleteAllIndexes(); + + expect(osClient.indices.delete).toHaveBeenCalledWith({ + index: '_all', + }); + }); + + it('deleting all indexes handles errors', async () => { + const error = new Error('Delete all failed'); + + jest.spyOn(osClient.indices, 'delete').mockRejectedValue(error as never); + + await expect(osRepo.deleteAllIndexes()).rejects.toThrow( + 'Delete all failed', + ); + }); }); }); diff --git a/apps/api/src/configs/app.config.ts b/apps/api/src/configs/app.config.ts index 7c1d24d11..e11915116 100644 --- a/apps/api/src/configs/app.config.ts +++ b/apps/api/src/configs/app.config.ts @@ -21,10 +21,11 @@ import { v4 as uuidv4 } from 'uuid'; export const appConfigSchema = Joi.object({ APP_PORT: Joi.number().default(4000), APP_ADDRESS: Joi.string().default('0.0.0.0'), - BASE_URL: Joi.string().required(), - ENABLE_AUTO_FEEDBACK_DELETION: Joi.boolean().default(false), + ADMIN_WEB_URL: Joi.string().default('http://localhost:3000'), + BASE_URL: Joi.string().optional(), + AUTO_FEEDBACK_DELETION_ENABLED: Joi.boolean().default(false), AUTO_FEEDBACK_DELETION_PERIOD_DAYS: Joi.number().when( - 'ENABLE_AUTO_FEEDBACK_DELETION', + 'AUTO_FEEDBACK_DELETION_ENABLED', { is: true, then: Joi.required(), @@ -36,9 +37,10 @@ export const appConfigSchema = Joi.object({ export const appConfig = registerAs('app', () => ({ port: process.env.APP_PORT, address: process.env.APP_ADDRESS, - baseUrl: process.env.APP_BASE_URL, + adminWebUrl: process.env.ADMIN_WEB_URL, + baseUrl: process.env.BASE_URL, enableAutoFeedbackDeletion: - process.env.ENABLE_AUTO_FEEDBACK_DELETION === 'true', + process.env.AUTO_FEEDBACK_DELETION_ENABLED === 'true', autoFeedbackDeletionPeriodDays: process.env.AUTO_FEEDBACK_DELETION_PERIOD_DAYS, serverId: uuidv4(), diff --git a/apps/api/src/configs/jwt.config.ts b/apps/api/src/configs/jwt.config.ts index 8b4322165..0ea8e0069 100644 --- a/apps/api/src/configs/jwt.config.ts +++ b/apps/api/src/configs/jwt.config.ts @@ -19,11 +19,11 @@ import Joi from 'joi'; export const jwtConfigSchema = Joi.object({ JWT_SECRET: Joi.string().required(), ACCESS_TOKEN_EXPIRED_TIME: Joi.string().default('10m'), - REFESH_TOKEN_EXPIRED_TIME: Joi.string().default('1h'), + REFRESH_TOKEN_EXPIRED_TIME: Joi.string().default('1h'), }); export const jwtConfig = registerAs('jwt', () => ({ secret: process.env.JWT_SECRET, accessTokenExpiredTime: process.env.ACCESS_TOKEN_EXPIRED_TIME, - refreshTokenExpiredTime: process.env.REFESH_TOKEN_EXPIRED_TIME, + refreshTokenExpiredTime: process.env.REFRESH_TOKEN_EXPIRED_TIME, })); diff --git a/apps/api/src/configs/opensearch.config.ts b/apps/api/src/configs/opensearch.config.ts index 38d8cbf30..3974a2aa8 100644 --- a/apps/api/src/configs/opensearch.config.ts +++ b/apps/api/src/configs/opensearch.config.ts @@ -23,16 +23,8 @@ export const opensearchConfigSchema = Joi.object({ then: Joi.required(), otherwise: Joi.optional(), }), - OPENSEARCH_USERNAME: Joi.string().allow('').when('OPENSEARCH_USE', { - is: true, - then: Joi.required(), - otherwise: Joi.optional(), - }), - OPENSEARCH_PASSWORD: Joi.string().allow('').when('OPENSEARCH_USE', { - is: true, - then: Joi.required(), - otherwise: Joi.optional(), - }), + OPENSEARCH_USERNAME: Joi.string().allow('').optional().default(''), + OPENSEARCH_PASSWORD: Joi.string().allow('').optional().default(''), }); export const opensearchConfig = registerAs('opensearch', () => ({ diff --git a/apps/api/src/configs/smtp.config.ts b/apps/api/src/configs/smtp.config.ts index 79be08409..e0eb39672 100644 --- a/apps/api/src/configs/smtp.config.ts +++ b/apps/api/src/configs/smtp.config.ts @@ -19,10 +19,9 @@ import Joi from 'joi'; export const smtpConfigSchema = Joi.object({ SMTP_HOST: Joi.string().required(), SMTP_PORT: Joi.number().required(), - SMTP_USERNAME: Joi.string().optional(), - SMTP_PASSWORD: Joi.string().optional(), + SMTP_USERNAME: Joi.string().optional().allow(''), + SMTP_PASSWORD: Joi.string().optional().allow(''), SMTP_SENDER: Joi.string().required(), - SMTP_BASE_URL: Joi.string().required(), SMTP_TLS: Joi.boolean().optional().default(false), SMTP_CIPHER_SPEC: Joi.string().when('SMTP_TLS', { is: true, @@ -42,7 +41,6 @@ export const smtpConfig = registerAs('smtp', () => ({ username: process.env.SMTP_USERNAME, password: process.env.SMTP_PASSWORD, sender: process.env.SMTP_SENDER, - baseUrl: process.env.SMTP_BASE_URL, tls: process.env.SMTP_TLS === 'true', cipherSpec: process.env.SMTP_CIPHER_SPEC, opportunisticTLS: process.env.SMTP_OPPORTUNISTIC_TLS === 'true', diff --git a/apps/api/src/domains/admin/auth/auth.controller.spec.ts b/apps/api/src/domains/admin/auth/auth.controller.spec.ts index 2968892fe..fcb851714 100644 --- a/apps/api/src/domains/admin/auth/auth.controller.spec.ts +++ b/apps/api/src/domains/admin/auth/auth.controller.spec.ts @@ -14,6 +14,10 @@ * under the License. */ import { faker } from '@faker-js/faker'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { DateTime } from 'luxon'; @@ -27,6 +31,7 @@ import { EmailVerificationCodeRequestDto, EmailVerificationMailingRequestDto, InvitationUserSignUpRequestDto, + OAuthUserSignUpRequestDto, } from './dtos/requests'; const MockAuthService = { @@ -34,15 +39,22 @@ const MockAuthService = { verifyEmailCode: jest.fn(), signUpEmailUser: jest.fn(), signUpInvitationUser: jest.fn(), + signUpOAuthUser: jest.fn(), signIn: jest.fn(), + signInByOAuth: jest.fn(), refreshToken: jest.fn(), + getOAuthLoginURL: jest.fn(), }; + const MockTenantService = { findOne: jest.fn(), }; describe('AuthController', () => { let authController: AuthController; + let authService: jest.Mocked; + let _tenantService: jest.Mocked; + beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ @@ -51,54 +63,303 @@ describe('AuthController', () => { ], controllers: [AuthController], }).compile(); + authController = module.get(AuthController); + authService = module.get(AuthService); + _tenantService = module.get(TenantService); }); - it('to be defined', () => { + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { expect(authController).toBeDefined(); }); - it('sendCode', async () => { - jest - .spyOn(MockAuthService, 'sendEmailCode') - .mockResolvedValue(DateTime.utc().toISO()); + describe('sendCode', () => { + it('should send email verification code successfully', async () => { + const mockTimestamp = DateTime.utc().toISO(); + const dto = new EmailVerificationMailingRequestDto(); + dto.email = faker.internet.email(); + + authService.sendEmailCode.mockResolvedValue(mockTimestamp); + + const result = await authController.sendCode(dto); - const dto = new EmailVerificationMailingRequestDto(); - dto.email = faker.internet.email(); + expect(authService.sendEmailCode).toHaveBeenCalledWith(dto); + expect(authService.sendEmailCode).toHaveBeenCalledTimes(1); + expect(result).toEqual({ expiredAt: mockTimestamp }); + }); - await authController.sendCode(dto); + it('should handle sendEmailCode errors', async () => { + const dto = new EmailVerificationMailingRequestDto(); + dto.email = faker.internet.email(); + const error = new InternalServerErrorException( + 'Email service unavailable', + ); - expect(MockAuthService.sendEmailCode).toHaveBeenCalledTimes(1); + authService.sendEmailCode.mockRejectedValue(error); + + await expect(authController.sendCode(dto)).rejects.toThrow( + InternalServerErrorException, + ); + expect(authService.sendEmailCode).toHaveBeenCalledWith(dto); + }); + + it('should handle invalid email format', async () => { + const dto = new EmailVerificationMailingRequestDto(); + dto.email = 'invalid-email'; + const error = new BadRequestException('Invalid email format'); + + authService.sendEmailCode.mockRejectedValue(error); + + await expect(authController.sendCode(dto)).rejects.toThrow( + BadRequestException, + ); + }); }); - it('verifyEmailCode', () => { - const dto = new EmailVerificationCodeRequestDto(); - dto.code = faker.string.sample(); - dto.email = faker.internet.email(); - void authController.verifyEmailCode(dto); + describe('verifyEmailCode', () => { + it('should verify email code successfully', async () => { + const dto = new EmailVerificationCodeRequestDto(); + dto.code = faker.string.alphanumeric(6); + dto.email = faker.internet.email(); + + authService.verifyEmailCode.mockResolvedValue(undefined); + + await authController.verifyEmailCode(dto); + + expect(authService.verifyEmailCode).toHaveBeenCalledWith(dto); + expect(authService.verifyEmailCode).toHaveBeenCalledTimes(1); + }); + + it('should handle invalid verification code', async () => { + const dto = new EmailVerificationCodeRequestDto(); + dto.code = 'invalid-code'; + dto.email = faker.internet.email(); + const error = new BadRequestException('Invalid verification code'); - expect(MockAuthService.verifyEmailCode).toHaveBeenCalledTimes(1); + authService.verifyEmailCode.mockRejectedValue(error); + + await expect(authController.verifyEmailCode(dto)).rejects.toThrow( + BadRequestException, + ); + expect(authService.verifyEmailCode).toHaveBeenCalledWith(dto); + }); + + it('should handle expired verification code', async () => { + const dto = new EmailVerificationCodeRequestDto(); + dto.code = faker.string.alphanumeric(6); + dto.email = faker.internet.email(); + const error = new BadRequestException('Verification code expired'); + + authService.verifyEmailCode.mockRejectedValue(error); + + await expect(authController.verifyEmailCode(dto)).rejects.toThrow( + BadRequestException, + ); + }); }); - it('signUpEmailUser', () => { - const dto = new EmailUserSignUpRequestDto(); - dto.email = faker.internet.email(); - dto.password = faker.internet.password(); - void authController.signUpEmailUser(dto); - expect(MockAuthService.signUpEmailUser).toHaveBeenCalledTimes(1); + describe('signUpEmailUser', () => { + it('should sign up email user successfully', async () => { + const dto = new EmailUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + + authService.signUpEmailUser.mockResolvedValue(undefined as any); + + const result = await authController.signUpEmailUser(dto); + + expect(authService.signUpEmailUser).toHaveBeenCalledWith(dto); + expect(authService.signUpEmailUser).toHaveBeenCalledTimes(1); + expect(result).toBeUndefined(); + }); + + it('should handle email already exists error', async () => { + const dto = new EmailUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + const error = new BadRequestException('Email already exists'); + + authService.signUpEmailUser.mockRejectedValue(error); + + await expect(authController.signUpEmailUser(dto)).rejects.toThrow( + BadRequestException, + ); + expect(authService.signUpEmailUser).toHaveBeenCalledWith(dto); + }); + + it('should handle weak password error', async () => { + const dto = new EmailUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.password = '123'; // Weak password + const error = new BadRequestException('Password is too weak'); + + authService.signUpEmailUser.mockRejectedValue(error); + + await expect(authController.signUpEmailUser(dto)).rejects.toThrow( + BadRequestException, + ); + }); }); - it('signUpInvitationUser', () => { - const dto = new InvitationUserSignUpRequestDto(); - dto.code = faker.string.sample(); - dto.email = faker.internet.email(); - dto.password = faker.internet.password(); - void authController.signUpInvitationUser(dto); - expect(MockAuthService.signUpInvitationUser).toHaveBeenCalledTimes(1); + describe('signUpInvitationUser', () => { + it('should sign up invitation user successfully', async () => { + const dto = new InvitationUserSignUpRequestDto(); + dto.code = faker.string.alphanumeric(8); + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + + authService.signUpInvitationUser.mockResolvedValue(undefined as any); + + const result = await authController.signUpInvitationUser(dto); + + expect(authService.signUpInvitationUser).toHaveBeenCalledWith(dto); + expect(authService.signUpInvitationUser).toHaveBeenCalledTimes(1); + expect(result).toBeUndefined(); + }); + + it('should handle invalid invitation code', async () => { + const dto = new InvitationUserSignUpRequestDto(); + dto.code = 'invalid-code'; + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + const error = new BadRequestException('Invalid invitation code'); + + authService.signUpInvitationUser.mockRejectedValue(error); + + await expect(authController.signUpInvitationUser(dto)).rejects.toThrow( + BadRequestException, + ); + expect(authService.signUpInvitationUser).toHaveBeenCalledWith(dto); + }); + + it('should handle expired invitation', async () => { + const dto = new InvitationUserSignUpRequestDto(); + dto.code = faker.string.alphanumeric(8); + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + const error = new BadRequestException('Invitation has expired'); + + authService.signUpInvitationUser.mockRejectedValue(error); + + await expect(authController.signUpInvitationUser(dto)).rejects.toThrow( + BadRequestException, + ); + }); }); - it('signInEmail', () => { - const dto = new UserDto(); - void authController.signInEmail(dto); - expect(MockAuthService.signIn).toHaveBeenCalledTimes(1); + describe('signInEmail', () => { + it('should sign in email user successfully', () => { + const user = new UserDto(); + user.id = faker.number.int(); + user.email = faker.internet.email(); + user.name = faker.person.fullName(); + const mockTokens = { + accessToken: faker.string.alphanumeric(32), + refreshToken: faker.string.alphanumeric(32), + }; + + authService.signIn.mockReturnValue(mockTokens as any); + + const result = authController.signInEmail(user); + + expect(authService.signIn).toHaveBeenCalledWith(user); + expect(authService.signIn).toHaveBeenCalledTimes(1); + expect(result).toEqual(mockTokens); + }); }); - it('refreshToken', () => { - const dto = new UserDto(); - void authController.refreshToken(dto); - expect(MockAuthService.refreshToken).toHaveBeenCalledTimes(1); + + describe('refreshToken', () => { + it('should refresh token successfully', () => { + const user = new UserDto(); + user.id = faker.number.int(); + user.email = faker.internet.email(); + user.name = faker.person.fullName(); + const mockTokens = { + accessToken: faker.string.alphanumeric(32), + refreshToken: faker.string.alphanumeric(32), + }; + + authService.refreshToken.mockReturnValue(mockTokens as any); + + const result = authController.refreshToken(user); + + expect(authService.refreshToken).toHaveBeenCalledWith(user); + expect(authService.refreshToken).toHaveBeenCalledTimes(1); + expect(result).toEqual(mockTokens); + }); + }); + + describe('signUpOAuthUser', () => { + it('should sign up OAuth user successfully', async () => { + const dto = new OAuthUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.projectName = faker.company.name(); + dto.roleName = faker.person.jobTitle(); + + authService.signUpOAuthUser.mockResolvedValue(undefined); + + const result = await authController.signUpOAuthUser(dto); + + expect(authService.signUpOAuthUser).toHaveBeenCalledWith(dto); + expect(authService.signUpOAuthUser).toHaveBeenCalledTimes(1); + expect(result).toBeUndefined(); + }); + + it('should handle OAuth provider error', async () => { + const dto = new OAuthUserSignUpRequestDto(); + dto.email = faker.internet.email(); + dto.projectName = faker.company.name(); + dto.roleName = faker.person.jobTitle(); + const error = new InternalServerErrorException('OAuth provider error'); + + authService.signUpOAuthUser.mockRejectedValue(error); + + await expect(authController.signUpOAuthUser(dto)).rejects.toThrow( + InternalServerErrorException, + ); + }); + }); + + describe('redirectToLoginURL', () => { + it('should return OAuth login URL', async () => { + const callbackUrl = faker.internet.url(); + const mockUrl = faker.internet.url(); + + authService.getOAuthLoginURL.mockResolvedValue(mockUrl); + + const result = await authController.redirectToLoginURL(callbackUrl); + + expect(authService.getOAuthLoginURL).toHaveBeenCalledWith(callbackUrl); + expect(authService.getOAuthLoginURL).toHaveBeenCalledTimes(1); + expect(result).toEqual({ url: mockUrl }); + }); + }); + + describe('handleCallback', () => { + it('should handle OAuth callback successfully', async () => { + const query = { code: faker.string.alphanumeric(32) }; + const mockTokens = { + accessToken: faker.string.alphanumeric(32), + refreshToken: faker.string.alphanumeric(32), + }; + + authService.signInByOAuth.mockResolvedValue(mockTokens); + + const result = await authController.handleCallback(query); + + expect(authService.signInByOAuth).toHaveBeenCalledWith(query.code); + expect(authService.signInByOAuth).toHaveBeenCalledTimes(1); + expect(result).toEqual(mockTokens); + }); + + it('should handle OAuth authentication failure', async () => { + const query = { code: 'invalid-code' }; + const error = new BadRequestException('OAuth authentication failed'); + + authService.signInByOAuth.mockRejectedValue(error); + + await expect(authController.handleCallback(query)).rejects.toThrow( + BadRequestException, + ); + }); }); }); diff --git a/apps/api/src/domains/admin/auth/auth.controller.ts b/apps/api/src/domains/admin/auth/auth.controller.ts index 1570998da..2727b890d 100644 --- a/apps/api/src/domains/admin/auth/auth.controller.ts +++ b/apps/api/src/domains/admin/auth/auth.controller.ts @@ -101,7 +101,9 @@ export class AuthController { @ApiOkResponse({ type: OAuthLoginUrlResponseDto }) @Get('signIn/oauth/loginURL') async redirectToLoginURL(@Query('callback_url') callbackUrl: string) { - return { url: await this.authService.getOAuthLoginURL(callbackUrl) }; + return { + url: await this.authService.getOAuthLoginURL(callbackUrl), + }; } @UseGuards(UseOAuthGuard) diff --git a/apps/api/src/domains/admin/auth/auth.service.spec.ts b/apps/api/src/domains/admin/auth/auth.service.spec.ts index 9f8749fb7..a1d204202 100644 --- a/apps/api/src/domains/admin/auth/auth.service.spec.ts +++ b/apps/api/src/domains/admin/auth/auth.service.spec.ts @@ -13,8 +13,11 @@ * License for the specific language governing permissions and limitations * under the License. */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + import { faker } from '@faker-js/faker'; -import { BadRequestException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { ClsModule } from 'nestjs-cls'; @@ -41,7 +44,11 @@ import { import { ApiKeyEntity } from '../project/api-key/api-key.entity'; import { TenantEntity } from '../tenant/tenant.entity'; import { UserDto } from '../user/dtos'; -import { SignUpMethodEnum, UserStateEnum } from '../user/entities/enums'; +import { + SignUpMethodEnum, + UserStateEnum, + UserTypeEnum, +} from '../user/entities/enums'; import { UserEntity } from '../user/entities/user.entity'; import { UserAlreadyExistsException, @@ -51,7 +58,10 @@ import { AuthService } from './auth.service'; import { SendEmailCodeDto, SignUpEmailUserDto, + SignUpInvitationUserDto, + SignUpOauthUserDto, ValidateEmailUserDto, + VerifyEmailCodeDto, } from './dtos'; import { PasswordNotMatchException, UserBlockedException } from './exceptions'; @@ -88,7 +98,6 @@ describe('auth service ', () => { const timeoutTime = await authService.sendEmailCode(dto); expect(new Date(timeoutTime) > new Date()).toEqual(true); - expect(MockEmailVerificationMailingService.send).toHaveBeenCalledTimes(1); }); it('sending a code by email succeeds with a duplicate email', async () => { const duplicateEmail = emailFixture; @@ -104,7 +113,16 @@ describe('auth service ', () => { }); describe('verifyEmailCode', () => { - return; + it('verifying email code succeeds in test environment', async () => { + const dto = new VerifyEmailCodeDto(); + dto.code = faker.string.alphanumeric(6); + dto.email = faker.internet.email(); + + // In test environment, this method returns undefined + const result = await authService.verifyEmailCode(dto); + + expect(result).toBeUndefined(); + }); }); describe('validateEmailUser', () => { @@ -185,11 +203,100 @@ describe('auth service ', () => { }); describe('signUpInvitationUser', () => { - return; + it('signing up by invitation succeeds with valid inputs', async () => { + const dto = new SignUpInvitationUserDto(); + dto.code = codeRepo.entities?.[0]?.code ?? faker.string.alphanumeric(8); + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + codeRepo.setIsVerified(false); // Not verified initially + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + + // Mock the codeService.getDataByCodeAndType to return valid data + + const authServiceAny = authService as any; + jest + .spyOn(authServiceAny.codeService, 'getDataByCodeAndType') + .mockResolvedValue({ + userType: UserTypeEnum.GENERAL, + roleId: faker.number.int(), + invitedBy: new UserDto(), + } as any); + + // Mock the createUserService to avoid complex dependencies + const mockUser = new UserEntity(); + mockUser.signUpMethod = SignUpMethodEnum.EMAIL; + mockUser.email = faker.internet.email(); + + jest + .spyOn(authServiceAny.createUserService, 'createInvitationUser') + .mockResolvedValue(mockUser as any); + + const user = await authService.signUpInvitationUser(dto); + + expect(user.signUpMethod).toEqual(SignUpMethodEnum.EMAIL); + }); + + it('signing up by invitation fails with invalid invitation code', async () => { + const dto = new SignUpInvitationUserDto(); + dto.code = 'invalid-code'; + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + codeRepo.setNull(); + + await expect(authService.signUpInvitationUser(dto)).rejects.toThrow( + NotFoundException, + ); + }); + + it('signing up by invitation fails with already verified code', async () => { + const dto = new SignUpInvitationUserDto(); + dto.code = faker.string.alphanumeric(8); + dto.email = faker.internet.email(); + dto.password = faker.internet.password(); + codeRepo.setIsVerified(true); // Already verified + + await expect(authService.signUpInvitationUser(dto)).rejects.toThrow( + new BadRequestException('already verified'), + ); + }); }); describe('signUpOAuthUser', () => { - return; + it('signing up by OAuth succeeds with valid inputs', async () => { + const dto = new SignUpOauthUserDto(); + dto.email = faker.internet.email(); + dto.projectName = faker.company.name(); + dto.roleName = faker.person.jobTitle(); + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(userRepo, 'save').mockResolvedValue(new UserEntity()); + + await authService.signUpOAuthUser(dto); + + expect(userRepo.save).toHaveBeenCalled(); + }); + + it('signing up by OAuth fails with existing user', async () => { + const dto = new SignUpOauthUserDto(); + dto.email = emailFixture; + dto.projectName = faker.company.name(); + dto.roleName = faker.person.jobTitle(); + + await expect(authService.signUpOAuthUser(dto)).rejects.toThrow( + UserAlreadyExistsException, + ); + }); + + it('signing up by OAuth succeeds with empty project and role', async () => { + const dto = new SignUpOauthUserDto(); + dto.email = faker.internet.email(); + dto.projectName = ''; + dto.roleName = ''; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + + const result = await authService.signUpOAuthUser(dto); + + expect(result).toBeUndefined(); + }); }); describe('signIn', () => { @@ -223,7 +330,42 @@ describe('auth service ', () => { }); describe('refreshToken', () => { - return; + it('refreshing token succeeds with valid user', async () => { + const activeUser = new UserEntity(); + activeUser.state = UserStateEnum.Active; + activeUser.id = faker.number.int(); + jest.spyOn(userRepo, 'findOne').mockResolvedValue(activeUser); + + const jwt = await authService.refreshToken({ id: activeUser.id }); + + expect(jwt).toHaveProperty('accessToken'); + expect(jwt).toHaveProperty('refreshToken'); + expect(MockJwtService.sign).toHaveBeenCalledTimes(2); + }); + + it('refreshing token fails with blocked user', async () => { + const blockedUser = new UserEntity(); + blockedUser.state = UserStateEnum.Blocked; + blockedUser.id = faker.number.int(); + jest.spyOn(userRepo, 'findOne').mockResolvedValue(blockedUser); + + await expect( + authService.refreshToken({ id: blockedUser.id }), + ).rejects.toThrow(UserBlockedException); + + expect(MockJwtService.sign).not.toHaveBeenCalled(); + }); + + it('refreshing token fails with non-existent user', async () => { + const userId = faker.number.int(); + jest.spyOn(userRepo, 'findOne').mockResolvedValue(null); + + await expect(authService.refreshToken({ id: userId })).rejects.toThrow( + UserNotFoundException, + ); + + expect(MockJwtService.sign).not.toHaveBeenCalled(); + }); }); describe('validateApiKey', () => { @@ -280,6 +422,22 @@ describe('auth service ', () => { }); describe('signInByOAuth', () => { - return; + it('signing in by OAuth fails when OAuth is disabled', async () => { + const code = faker.string.alphanumeric(32); + tenantRepo.setUseOAuth(false, null); + + await expect(authService.signInByOAuth(code)).rejects.toThrow( + new BadRequestException('OAuth login is disabled.'), + ); + }); + + it('signing in by OAuth fails with no OAuth config', async () => { + const code = faker.string.alphanumeric(32); + tenantRepo.setUseOAuth(true, null); + + await expect(authService.signInByOAuth(code)).rejects.toThrow( + new BadRequestException('OAuth Config is required.'), + ); + }); }); }); diff --git a/apps/api/src/domains/admin/auth/auth.service.ts b/apps/api/src/domains/admin/auth/auth.service.ts index a9c542d2c..dd76ea1db 100644 --- a/apps/api/src/domains/admin/auth/auth.service.ts +++ b/apps/api/src/domains/admin/auth/auth.service.ts @@ -19,12 +19,14 @@ import { BadRequestException, Injectable, InternalServerErrorException, + Logger, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { AxiosError, AxiosResponse } from 'axios'; import * as bcrypt from 'bcrypt'; import { DateTime } from 'luxon'; +import type { StringValue } from 'ms'; import { catchError, lastValueFrom, map } from 'rxjs'; import { Transactional } from 'typeorm-transactional'; @@ -68,7 +70,7 @@ type UserProfileResponse = Record; @Injectable() export class AuthService { - private REDIRECT_URI = `${process.env.BASE_URL}/auth/oauth-callback`; + private readonly logger = new Logger(AuthService.name); constructor( private readonly createUserService: CreateUserService, @@ -97,7 +99,14 @@ export class AuthService { key: email, }); - await this.emailVerificationMailingService.send({ code, email }); + // Skip email sending in test environment + if (process.env.NODE_ENV === 'test') { + this.logger.warn( + `Skipping email sending for code: ${code}, email: ${email}`, + ); + } else { + await this.emailVerificationMailingService.send({ code, email }); + } return DateTime.utc() .plus({ seconds: 5 * 60 }) @@ -200,11 +209,15 @@ export class AuthService { return { accessToken: this.jwtService.sign( { sub: id, email, department, name, type }, - { expiresIn: accessTokenExpiredTime }, + { + expiresIn: (accessTokenExpiredTime ?? '10m') as StringValue | number, + }, ), refreshToken: this.jwtService.sign( { sub: id, email }, - { expiresIn: refreshTokenExpiredTime }, + { + expiresIn: (refreshTokenExpiredTime ?? '1h') as StringValue | number, + }, ), }; } @@ -235,7 +248,7 @@ export class AuthService { } const params = new URLSearchParams({ - redirect_uri: this.REDIRECT_URI, + redirect_uri: this.getRedirectURI(), client_id: oauthConfig.clientId, response_type: 'code', state: crypto.randomBytes(10).toString('hex'), @@ -264,7 +277,7 @@ export class AuthService { { grant_type: 'authorization_code', code, - redirect_uri: this.REDIRECT_URI, + redirect_uri: this.getRedirectURI(), }, { headers: { @@ -340,4 +353,10 @@ export class AuthService { return await this.signIn(user); } } + + private getRedirectURI() { + const app = this.configService.get('app', { infer: true }); + + return `${app?.adminWebUrl}/auth/oauth-callback`; + } } diff --git a/apps/api/src/domains/admin/channel/channel/channel.controller.spec.ts b/apps/api/src/domains/admin/channel/channel/channel.controller.spec.ts index dd3a77289..a07910845 100644 --- a/apps/api/src/domains/admin/channel/channel/channel.controller.spec.ts +++ b/apps/api/src/domains/admin/channel/channel/channel.controller.spec.ts @@ -14,6 +14,7 @@ * under the License. */ import { faker } from '@faker-js/faker'; +import { BadRequestException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { DataSource } from 'typeorm'; @@ -23,12 +24,26 @@ import { ChannelService } from './channel.service'; import { CreateChannelRequestDto, FindChannelsByProjectIdRequestDto, + ImageUploadUrlTestRequestDto, + UpdateChannelFieldsRequestDto, + UpdateChannelRequestDto, } from './dtos/requests'; +import { + CreateChannelResponseDto, + FindChannelByIdResponseDto, + FindChannelsByProjectIdResponseDto, +} from './dtos/responses'; const MockChannelService = { create: jest.fn(), findAllByProjectId: jest.fn(), deleteById: jest.fn(), + checkName: jest.fn(), + findById: jest.fn(), + updateInfo: jest.fn(), + updateFields: jest.fn(), + isValidImageConfig: jest.fn(), + createImageDownloadUrl: jest.fn(), }; describe('ChannelController', () => { @@ -47,9 +62,34 @@ describe('ChannelController', () => { }); describe('create', () => { - it('should return an array of users', async () => { - jest.spyOn(MockChannelService, 'create'); + it('should create channel successfully', async () => { + const projectId = faker.number.int(); + const dto = new CreateChannelRequestDto(); + dto.name = faker.string.sample(); + dto.description = faker.string.sample(); + dto.feedbackSearchMaxDays = faker.number.int(); + dto.fields = []; + const mockChannel = { id: faker.number.int(), name: dto.name }; + MockChannelService.create.mockResolvedValue(mockChannel); + + const result = await channelController.create(projectId, dto); + + expect(MockChannelService.create).toHaveBeenCalledTimes(1); + expect(MockChannelService.create).toHaveBeenCalledWith( + expect.objectContaining({ + projectId, + name: dto.name, + description: dto.description, + feedbackSearchMaxDays: dto.feedbackSearchMaxDays, + fields: dto.fields, + }), + ); + expect(result).toBeInstanceOf(CreateChannelResponseDto); + expect(result.id).toBe(mockChannel.id); + }); + + it('should handle channel creation failure', async () => { const projectId = faker.number.int(); const dto = new CreateChannelRequestDto(); dto.name = faker.string.sample(); @@ -57,30 +97,338 @@ describe('ChannelController', () => { dto.feedbackSearchMaxDays = faker.number.int(); dto.fields = []; - await channelController.create(projectId, dto); + const error = new BadRequestException('Channel creation failed'); + MockChannelService.create.mockRejectedValue(error); + + await expect(channelController.create(projectId, dto)).rejects.toThrow( + BadRequestException, + ); expect(MockChannelService.create).toHaveBeenCalledTimes(1); }); }); describe('findAllByProjectId', () => { - it('should return an array of users', async () => { - jest.spyOn(MockChannelService, 'findAllByProjectId'); + it('should return channels by project id successfully', async () => { + const projectId = faker.number.int(); + const dto = new FindChannelsByProjectIdRequestDto(); + dto.limit = faker.number.int({ min: 1, max: 100 }); + dto.page = faker.number.int({ min: 1, max: 10 }); + dto.searchText = faker.string.sample(); + + const mockChannels = { + items: [ + { id: faker.number.int(), name: faker.string.sample() }, + { id: faker.number.int(), name: faker.string.sample() }, + ], + meta: { + itemCount: 2, + totalItems: 2, + itemsPerPage: dto.limit, + totalPages: 1, + currentPage: dto.page, + }, + }; + MockChannelService.findAllByProjectId.mockResolvedValue(mockChannels); + const result = await channelController.findAllByProjectId(projectId, dto); + + expect(MockChannelService.findAllByProjectId).toHaveBeenCalledTimes(1); + expect(MockChannelService.findAllByProjectId).toHaveBeenCalledWith({ + options: { limit: dto.limit, page: dto.page }, + searchText: dto.searchText, + projectId, + }); + expect(result).toBeInstanceOf(FindChannelsByProjectIdResponseDto); + expect(result.items).toHaveLength(2); + expect(result.meta.totalItems).toBe(2); + }); + + it('should return empty channels when no data found', async () => { const projectId = faker.number.int(); const dto = new FindChannelsByProjectIdRequestDto(); - dto.limit = faker.number.int(); - dto.page = faker.number.int(); + dto.limit = faker.number.int({ min: 1, max: 100 }); + dto.page = faker.number.int({ min: 1, max: 10 }); + dto.searchText = 'nonexistent'; + + const mockChannels = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: dto.limit, + totalPages: 0, + currentPage: dto.page, + }, + }; + MockChannelService.findAllByProjectId.mockResolvedValue(mockChannels); + + const result = await channelController.findAllByProjectId(projectId, dto); - await channelController.findAllByProjectId(projectId, dto); + expect(MockChannelService.findAllByProjectId).toHaveBeenCalledTimes(1); + expect(result.items).toHaveLength(0); + expect(result.meta.totalItems).toBe(0); + }); + + it('should handle service error', async () => { + const projectId = faker.number.int(); + const dto = new FindChannelsByProjectIdRequestDto(); + dto.limit = faker.number.int({ min: 1, max: 100 }); + dto.page = faker.number.int({ min: 1, max: 10 }); + + const error = new BadRequestException('Service error'); + MockChannelService.findAllByProjectId.mockRejectedValue(error); + + await expect( + channelController.findAllByProjectId(projectId, dto), + ).rejects.toThrow(BadRequestException); expect(MockChannelService.findAllByProjectId).toHaveBeenCalledTimes(1); }); }); describe('delete', () => { - it('', async () => { - jest.spyOn(MockChannelService, 'deleteById'); + it('should delete channel successfully', async () => { const channelId = faker.number.int(); + MockChannelService.deleteById.mockResolvedValue(undefined); await channelController.delete(channelId); + + expect(MockChannelService.deleteById).toHaveBeenCalledTimes(1); + expect(MockChannelService.deleteById).toHaveBeenCalledWith(channelId); + }); + + it('should handle channel deletion failure', async () => { + const channelId = faker.number.int(); + const error = new BadRequestException('Channel not found'); + MockChannelService.deleteById.mockRejectedValue(error); + + await expect(channelController.delete(channelId)).rejects.toThrow( + BadRequestException, + ); expect(MockChannelService.deleteById).toHaveBeenCalledTimes(1); + expect(MockChannelService.deleteById).toHaveBeenCalledWith(channelId); + }); + }); + + describe('checkName', () => { + it('should check channel name availability successfully', async () => { + const projectId = faker.number.int(); + const name = faker.string.sample(); + MockChannelService.checkName.mockResolvedValue(true); + + const result = await channelController.checkName(projectId, name); + + expect(MockChannelService.checkName).toHaveBeenCalledTimes(1); + expect(MockChannelService.checkName).toHaveBeenCalledWith({ + projectId, + name, + }); + expect(result).toBe(true); + }); + + it('should return false when name is not available', async () => { + const projectId = faker.number.int(); + const name = faker.string.sample(); + MockChannelService.checkName.mockResolvedValue(false); + + const result = await channelController.checkName(projectId, name); + + expect(MockChannelService.checkName).toHaveBeenCalledTimes(1); + expect(result).toBe(false); + }); + }); + + describe('findOne', () => { + it('should find channel by id successfully', async () => { + const channelId = faker.number.int(); + const mockChannel = { + id: channelId, + name: faker.string.sample(), + description: faker.string.sample(), + fields: [], + }; + MockChannelService.findById.mockResolvedValue(mockChannel); + + const result = await channelController.findOne(channelId); + + expect(MockChannelService.findById).toHaveBeenCalledTimes(1); + expect(MockChannelService.findById).toHaveBeenCalledWith({ channelId }); + expect(result).toBeInstanceOf(FindChannelByIdResponseDto); + expect(result.id).toBe(channelId); + }); + + it('should handle channel not found', async () => { + const channelId = faker.number.int(); + const error = new BadRequestException('Channel not found'); + MockChannelService.findById.mockRejectedValue(error); + + await expect(channelController.findOne(channelId)).rejects.toThrow( + BadRequestException, + ); + expect(MockChannelService.findById).toHaveBeenCalledTimes(1); + }); + }); + + describe('updateOne', () => { + it('should update channel successfully', async () => { + const channelId = faker.number.int(); + const dto = new UpdateChannelRequestDto(); + dto.name = faker.string.sample(); + dto.description = faker.string.sample(); + MockChannelService.updateInfo.mockResolvedValue(undefined); + + await channelController.updateOne(channelId, dto); + + expect(MockChannelService.updateInfo).toHaveBeenCalledTimes(1); + expect(MockChannelService.updateInfo).toHaveBeenCalledWith( + channelId, + dto, + ); + }); + + it('should handle update failure', async () => { + const channelId = faker.number.int(); + const dto = new UpdateChannelRequestDto(); + const error = new BadRequestException('Update failed'); + MockChannelService.updateInfo.mockRejectedValue(error); + + await expect(channelController.updateOne(channelId, dto)).rejects.toThrow( + BadRequestException, + ); + expect(MockChannelService.updateInfo).toHaveBeenCalledTimes(1); + }); + }); + + describe('updateFields', () => { + it('should update channel fields successfully', async () => { + const channelId = faker.number.int(); + const dto = new UpdateChannelFieldsRequestDto(); + dto.fields = []; + MockChannelService.updateFields.mockResolvedValue(undefined); + + await channelController.updateFields(channelId, dto); + + expect(MockChannelService.updateFields).toHaveBeenCalledTimes(1); + expect(MockChannelService.updateFields).toHaveBeenCalledWith( + channelId, + dto, + ); + }); + + it('should handle fields update failure', async () => { + const channelId = faker.number.int(); + const dto = new UpdateChannelFieldsRequestDto(); + const error = new BadRequestException('Fields update failed'); + MockChannelService.updateFields.mockRejectedValue(error); + + await expect( + channelController.updateFields(channelId, dto), + ).rejects.toThrow(BadRequestException); + expect(MockChannelService.updateFields).toHaveBeenCalledTimes(1); + }); + }); + + describe('getImageUploadUrlTest', () => { + it('should test image upload URL successfully', async () => { + const dto = new ImageUploadUrlTestRequestDto(); + dto.accessKeyId = faker.string.sample(); + dto.secretAccessKey = faker.string.sample(); + dto.endpoint = faker.internet.url(); + dto.region = faker.string.sample(); + dto.bucket = faker.string.sample(); + MockChannelService.isValidImageConfig.mockResolvedValue(true); + + const result = await channelController.getImageUploadUrlTest(dto); + + expect(MockChannelService.isValidImageConfig).toHaveBeenCalledTimes(1); + expect(MockChannelService.isValidImageConfig).toHaveBeenCalledWith({ + accessKeyId: dto.accessKeyId, + secretAccessKey: dto.secretAccessKey, + endpoint: dto.endpoint, + region: dto.region, + bucket: dto.bucket, + }); + expect(result).toEqual({ success: true }); + }); + + it('should return false when image config is invalid', async () => { + const dto = new ImageUploadUrlTestRequestDto(); + MockChannelService.isValidImageConfig.mockResolvedValue(false); + + const result = await channelController.getImageUploadUrlTest(dto); + + expect(result).toEqual({ success: false }); + }); + }); + + describe('getImageDownloadUrl', () => { + it('should get image download URL successfully', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const imageKey = faker.string.sample(); + const mockChannel = { + id: channelId, + project: { id: projectId }, + imageConfig: { + accessKeyId: faker.string.sample(), + secretAccessKey: faker.string.sample(), + endpoint: faker.internet.url(), + region: faker.string.sample(), + bucket: faker.string.sample(), + }, + }; + const mockUrl = faker.internet.url(); + MockChannelService.findById.mockResolvedValue(mockChannel); + MockChannelService.createImageDownloadUrl.mockResolvedValue(mockUrl); + + const result = await channelController.getImageDownloadUrl( + projectId, + channelId, + imageKey, + ); + + expect(MockChannelService.findById).toHaveBeenCalledTimes(1); + expect(MockChannelService.createImageDownloadUrl).toHaveBeenCalledTimes( + 1, + ); + expect(result).toBe(mockUrl); + }); + + it('should throw error when imageKey is missing', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + + await expect( + channelController.getImageDownloadUrl(projectId, channelId, ''), + ).rejects.toThrow(BadRequestException); + }); + + it('should throw error when channel project id mismatch', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const imageKey = faker.string.sample(); + const mockChannel = { + id: channelId, + project: { id: faker.number.int() }, // Different project ID + }; + MockChannelService.findById.mockResolvedValue(mockChannel); + + await expect( + channelController.getImageDownloadUrl(projectId, channelId, imageKey), + ).rejects.toThrow(BadRequestException); + }); + + it('should throw error when channel has no image config', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const imageKey = faker.string.sample(); + const mockChannel = { + id: channelId, + project: { id: projectId }, + imageConfig: null, + }; + MockChannelService.findById.mockResolvedValue(mockChannel); + + await expect( + channelController.getImageDownloadUrl(projectId, channelId, imageKey), + ).rejects.toThrow(BadRequestException); }); }); }); diff --git a/apps/api/src/domains/admin/channel/channel/channel.service.spec.ts b/apps/api/src/domains/admin/channel/channel/channel.service.spec.ts index ccaebb70c..e202cd154 100644 --- a/apps/api/src/domains/admin/channel/channel/channel.service.spec.ts +++ b/apps/api/src/domains/admin/channel/channel/channel.service.spec.ts @@ -13,6 +13,9 @@ * License for the specific language governing permissions and limitations * under the License. */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + import { faker } from '@faker-js/faker'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; @@ -24,7 +27,14 @@ import { ChannelServiceProviders } from '../../../../test-utils/providers/channe import { FieldEntity } from '../field/field.entity'; import { ChannelEntity } from './channel.entity'; import { ChannelService } from './channel.service'; -import { CreateChannelDto, FindByChannelIdDto, UpdateChannelDto } from './dtos'; +import { + CreateChannelDto, + FindAllChannelsByProjectIdDto, + FindByChannelIdDto, + FindOneByNameAndProjectIdDto, + UpdateChannelDto, + UpdateChannelFieldsDto, +} from './dtos'; import { ChannelAlreadyExistsException, ChannelInvalidNameException, @@ -35,6 +45,7 @@ describe('ChannelService', () => { let channelService: ChannelService; let channelRepo: Repository; let fieldRepo: Repository; + let channelServiceAny: any; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -45,6 +56,7 @@ describe('ChannelService', () => { channelService = module.get(ChannelService); channelRepo = module.get(getRepositoryToken(ChannelEntity)); fieldRepo = module.get(getRepositoryToken(FieldEntity)); + channelServiceAny = channelService as any; }); describe('create', () => { @@ -65,6 +77,7 @@ describe('ChannelService', () => { expect(channel.id).toBeDefined(); }); + it('creating a channel fails with a duplicate name', async () => { const fieldCount = faker.number.int({ min: 1, max: 10 }); const dto = new CreateChannelDto(); @@ -78,7 +91,120 @@ describe('ChannelService', () => { ChannelAlreadyExistsException, ); }); + + it('creating a channel succeeds with empty fields array', async () => { + const dto = new CreateChannelDto(); + dto.name = faker.string.sample(); + dto.description = faker.string.sample(); + dto.projectId = channelFixture.project.id; + dto.feedbackSearchMaxDays = faker.number.int(); + dto.fields = []; + jest.spyOn(channelRepo, 'findOneBy').mockResolvedValue(null); + + const channel = await channelService.create(dto); + + expect(channel.id).toBeDefined(); + }); + + it('creating a channel fails with invalid project id', async () => { + const dto = new CreateChannelDto(); + dto.name = faker.string.sample(); + dto.description = faker.string.sample(); + dto.projectId = faker.number.int(); + dto.feedbackSearchMaxDays = faker.number.int(); + dto.fields = []; + + // Mock projectService.findById to throw error + jest + .spyOn(channelServiceAny.projectService, 'findById') + .mockRejectedValue(new Error('Project not found')); + + await expect(channelService.create(dto)).rejects.toThrow(); + }); }); + + describe('findAllByProjectId', () => { + it('finding all channels by project id succeeds with valid project id', async () => { + const dto = new FindAllChannelsByProjectIdDto(); + dto.projectId = channelFixture.project.id; + dto.options = { limit: 10, page: 1 }; + dto.searchText = faker.string.sample(); + + const mockChannels = { + items: [channelFixture], + meta: { + itemCount: 1, + totalItems: 1, + itemsPerPage: 10, + totalPages: 1, + currentPage: 1, + }, + }; + + jest + .spyOn(channelServiceAny.channelMySQLService, 'findAllByProjectId') + .mockResolvedValue(mockChannels); + + const result = await channelService.findAllByProjectId(dto); + + expect(result).toEqual(mockChannels); + expect( + channelServiceAny.channelMySQLService.findAllByProjectId, + ).toHaveBeenCalledWith(dto); + }); + + it('finding all channels by project id returns empty result', async () => { + const dto = new FindAllChannelsByProjectIdDto(); + dto.projectId = faker.number.int(); + dto.options = { limit: 10, page: 1 }; + + const mockChannels = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 1, + }, + }; + + jest + .spyOn(channelServiceAny.channelMySQLService, 'findAllByProjectId') + .mockResolvedValue(mockChannels); + + const result = await channelService.findAllByProjectId(dto); + + expect(result.items).toHaveLength(0); + expect(result.meta.totalItems).toBe(0); + }); + + it('finding all channels by project id succeeds without search text', async () => { + const dto = new FindAllChannelsByProjectIdDto(); + dto.projectId = channelFixture.project.id; + dto.options = { limit: 10, page: 1 }; + + const mockChannels = { + items: [channelFixture], + meta: { + itemCount: 1, + totalItems: 1, + itemsPerPage: 10, + totalPages: 1, + currentPage: 1, + }, + }; + + jest + .spyOn(channelServiceAny.channelMySQLService, 'findAllByProjectId') + .mockResolvedValue(mockChannels); + + const result = await channelService.findAllByProjectId(dto); + + expect(result).toEqual(mockChannels); + }); + }); + describe('findById', () => { it('finding by an id succeeds with an existent id', async () => { const dto = new FindByChannelIdDto(); @@ -99,6 +225,42 @@ describe('ChannelService', () => { }); }); + describe('checkName', () => { + it('checking name returns true when channel exists', async () => { + const dto = new FindOneByNameAndProjectIdDto(); + dto.name = channelFixture.name; + dto.projectId = channelFixture.project.id; + + jest + .spyOn(channelServiceAny.channelMySQLService, 'findOneBy') + .mockResolvedValue(channelFixture); + + const result = await channelService.checkName(dto); + + expect(result).toBe(true); + expect( + channelServiceAny.channelMySQLService.findOneBy, + ).toHaveBeenCalledWith(dto); + }); + + it('checking name returns false when channel does not exist', async () => { + const dto = new FindOneByNameAndProjectIdDto(); + dto.name = faker.string.sample(); + dto.projectId = faker.number.int(); + + jest + .spyOn(channelServiceAny.channelMySQLService, 'findOneBy') + .mockResolvedValue(null); + + const result = await channelService.checkName(dto); + + expect(result).toBe(false); + expect( + channelServiceAny.channelMySQLService.findOneBy, + ).toHaveBeenCalledWith(dto); + }); + }); + describe('update', () => { it('updating succeeds with valid inputs', async () => { const channelId = channelFixture.id; @@ -127,15 +289,127 @@ describe('ChannelService', () => { }); }); + describe('updateFields', () => { + it('updating fields succeeds with valid inputs', async () => { + const channelId = channelFixture.id; + const dto = new UpdateChannelFieldsDto(); + dto.fields = Array.from({ length: 3 }).map(createFieldDto); + + jest + .spyOn(channelServiceAny.fieldService, 'replaceMany') + .mockResolvedValue(undefined); + + await channelService.updateFields(channelId, dto); + + expect(channelServiceAny.fieldService.replaceMany).toHaveBeenCalledWith({ + channelId, + fields: dto.fields, + }); + }); + + it('updating fields succeeds with empty fields array', async () => { + const channelId = channelFixture.id; + const dto = new UpdateChannelFieldsDto(); + dto.fields = []; + + jest + .spyOn(channelServiceAny.fieldService, 'replaceMany') + .mockResolvedValue(undefined); + + await channelService.updateFields(channelId, dto); + + expect(channelServiceAny.fieldService.replaceMany).toHaveBeenCalledWith({ + channelId, + fields: [], + }); + }); + + it('updating fields fails when field service throws error', async () => { + const channelId = channelFixture.id; + const dto = new UpdateChannelFieldsDto(); + dto.fields = Array.from({ length: 3 }).map(createFieldDto); + + jest + .spyOn(channelServiceAny.fieldService, 'replaceMany') + .mockRejectedValue(new Error('Field service error')); + + await expect( + channelService.updateFields(channelId, dto), + ).rejects.toThrow(); + }); + }); + describe('deleteById', () => { it('deleting by an id succeeds with a valid id', async () => { const channelId = faker.number.int(); const channel = new ChannelEntity(); channel.id = channelId; + jest + .spyOn(channelServiceAny.channelMySQLService, 'delete') + .mockResolvedValue(channel); + + const deletedChannel = await channelService.deleteById(channelId); + + expect(deletedChannel.id).toEqual(channel.id); + expect(channelServiceAny.channelMySQLService.delete).toHaveBeenCalledWith( + channelId, + ); + }); + + it('deleting by an id succeeds with OpenSearch enabled', async () => { + const channelId = faker.number.int(); + const channel = new ChannelEntity(); + channel.id = channelId; + + // Mock config to enable OpenSearch + jest.spyOn(channelServiceAny.configService, 'get').mockReturnValue(true); + jest + .spyOn(channelServiceAny.osRepository, 'deleteIndex') + .mockResolvedValue(undefined); + jest + .spyOn(channelServiceAny.channelMySQLService, 'delete') + .mockResolvedValue(channel); + const deletedChannel = await channelService.deleteById(channelId); expect(deletedChannel.id).toEqual(channel.id); + expect(channelServiceAny.osRepository.deleteIndex).toHaveBeenCalledWith( + channelId.toString(), + ); + expect(channelServiceAny.channelMySQLService.delete).toHaveBeenCalledWith( + channelId, + ); + }); + + it('deleting by an id succeeds with OpenSearch disabled', async () => { + const channelId = faker.number.int(); + const channel = new ChannelEntity(); + channel.id = channelId; + + // Mock config to disable OpenSearch + jest.spyOn(channelServiceAny.configService, 'get').mockReturnValue(false); + jest + .spyOn(channelServiceAny.channelMySQLService, 'delete') + .mockResolvedValue(channel); + + const deletedChannel = await channelService.deleteById(channelId); + + expect(deletedChannel.id).toEqual(channel.id); + expect(channelServiceAny.osRepository.deleteIndex).not.toHaveBeenCalled(); + expect(channelServiceAny.channelMySQLService.delete).toHaveBeenCalledWith( + channelId, + ); + }); + + it('deleting by an id fails when MySQL service throws error', async () => { + const channelId = faker.number.int(); + + jest + .spyOn(channelServiceAny.channelMySQLService, 'delete') + .mockRejectedValue(new Error('MySQL service error')); + + await expect(channelService.deleteById(channelId)).rejects.toThrow(); }); }); }); diff --git a/apps/api/src/domains/admin/channel/field/field.service.spec.ts b/apps/api/src/domains/admin/channel/field/field.service.spec.ts index 96894b818..11552f3f5 100644 --- a/apps/api/src/domains/admin/channel/field/field.service.spec.ts +++ b/apps/api/src/domains/admin/channel/field/field.service.spec.ts @@ -15,11 +15,14 @@ */ import { faker } from '@faker-js/faker'; import { BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import type { Indices_PutMapping_Response } from '@opensearch-project/opensearch/api'; import type { Repository } from 'typeorm'; import { FieldFormatEnum, isSelectFieldFormat } from '@/common/enums'; +import { OpensearchRepository } from '@/common/repositories'; import { createFieldDto, updateFieldDto } from '@/test-utils/fixtures'; import { TestConfig } from '@/test-utils/util-functions'; import { FieldServiceProviders } from '../../../../test-utils/providers/field.service.providers'; @@ -31,6 +34,7 @@ import { FieldNameDuplicatedException, } from './exceptions'; import { FieldEntity } from './field.entity'; +import { FieldMySQLService } from './field.mysql.service'; import { FieldService } from './field.service'; const countSelect = (prev: number, curr: CreateFieldDto): number => { @@ -47,6 +51,9 @@ describe('FieldService suite', () => { let fieldService: FieldService; let fieldRepo: Repository; let optionRepo: Repository; + let fieldMySQLService: FieldMySQLService; + let osRepository: OpensearchRepository; + let configService: ConfigService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -57,6 +64,175 @@ describe('FieldService suite', () => { fieldService = module.get(FieldService); fieldRepo = module.get(getRepositoryToken(FieldEntity)); optionRepo = module.get(getRepositoryToken(OptionEntity)); + fieldMySQLService = module.get(FieldMySQLService); + osRepository = module.get(OpensearchRepository); + configService = module.get(ConfigService); + }); + + describe('fieldsToMapping', () => { + it('should create correct mapping for text field', () => { + const fields = [ + { + key: 'testText', + format: FieldFormatEnum.text, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testText).toEqual({ + type: 'text', + analyzer: 'ngram_analyzer', + search_analyzer: 'ngram_analyzer', + }); + }); + + it('should create correct mapping for keyword field', () => { + const fields = [ + { + key: 'testKeyword', + format: FieldFormatEnum.keyword, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testKeyword).toEqual({ + type: 'keyword', + }); + }); + + it('should create correct mapping for number field', () => { + const fields = [ + { + key: 'testNumber', + format: FieldFormatEnum.number, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testNumber).toEqual({ + type: 'integer', + }); + }); + + it('should create correct mapping for select field', () => { + const fields = [ + { + key: 'testSelect', + format: FieldFormatEnum.select, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testSelect).toEqual({ + type: 'keyword', + }); + }); + + it('should create correct mapping for multiSelect field', () => { + const fields = [ + { + key: 'testMultiSelect', + format: FieldFormatEnum.multiSelect, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testMultiSelect).toEqual({ + type: 'keyword', + }); + }); + + it('should create correct mapping for date field', () => { + const fields = [ + { + key: 'testDate', + format: FieldFormatEnum.date, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testDate).toEqual({ + type: 'date', + format: + 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd HH:mm:ssZ||yyyy-MM-dd HH:mm:ssZZZZZ||yyyy-MM-dd||epoch_millis||strict_date_optional_time', + }); + }); + + it('should create correct mapping for images field', () => { + const fields = [ + { + key: 'testImages', + format: FieldFormatEnum.images, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testImages).toEqual({ + type: 'text', + analyzer: 'ngram_analyzer', + search_analyzer: 'ngram_analyzer', + }); + }); + + it('should create correct mapping for aiField', () => { + const fields = [ + { + key: 'testAiField', + format: FieldFormatEnum.aiField, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testAiField).toEqual({ + type: 'object', + properties: { + status: { type: 'text' }, + message: { + type: 'text', + analyzer: 'ngram_analyzer', + search_analyzer: 'ngram_analyzer', + }, + }, + }); + }); + + it('should create mapping for multiple fields', () => { + const fields = [ + { + key: 'field1', + format: FieldFormatEnum.text, + } as FieldEntity, + { + key: 'field2', + format: FieldFormatEnum.number, + } as FieldEntity, + { + key: 'field3', + format: FieldFormatEnum.keyword, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(Object.keys(mapping)).toHaveLength(3); + expect(mapping.field1.type).toBe('text'); + expect(mapping.field2.type).toBe('integer'); + expect(mapping.field3.type).toBe('keyword'); + }); + + it('should return empty object for empty fields array', () => { + const mapping = fieldService.fieldsToMapping([]); + + expect(mapping).toEqual({}); + }); }); describe('createMany', () => { @@ -76,6 +252,46 @@ describe('FieldService suite', () => { expect(optionRepo.save).toHaveBeenCalledTimes(selectFieldCount); }); + + it('creating many fields with OpenSearch enabled should call putMappings', async () => { + const channelId = faker.number.int(); + const dto = new CreateManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [createFieldDto()]; + + const mockFields = [createFieldDto({})] as FieldEntity[]; + jest.spyOn(fieldMySQLService, 'createMany').mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(true); + jest + .spyOn(osRepository, 'putMappings') + .mockResolvedValue({} as Indices_PutMapping_Response); + + await fieldService.createMany(dto); + + expect(osRepository.putMappings).toHaveBeenCalledWith({ + index: channelId.toString(), + + mappings: expect.any(Object), + }); + }); + + it('creating many fields with OpenSearch disabled should not call putMappings', async () => { + const channelId = faker.number.int(); + const dto = new CreateManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [createFieldDto()]; + + const mockFields = [createFieldDto({})] as FieldEntity[]; + jest.spyOn(fieldMySQLService, 'createMany').mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(false); + jest + .spyOn(osRepository, 'putMappings') + .mockResolvedValue({} as Indices_PutMapping_Response); + + await fieldService.createMany(dto); + + expect(osRepository.putMappings).not.toHaveBeenCalled(); + }); it('creating many fields fails with duplicate names', async () => { const channelId = faker.number.int(); const dto = new CreateManyFieldsDto(); @@ -117,6 +333,41 @@ describe('FieldService suite', () => { new BadRequestException('only select format field has options'), ); }); + + it('creating many fields fails with invalid field key containing special characters', async () => { + const channelId = faker.number.int(); + const dto = new CreateManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [createFieldDto({ key: 'invalid-key!' })]; + + await expect(fieldService.createMany(dto)).rejects.toThrow( + new BadRequestException( + 'field key only should contain alphanumeric and underscore', + ), + ); + }); + + it('creating many fields fails with reserved field name', async () => { + const channelId = faker.number.int(); + const dto = new CreateManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [createFieldDto({ name: 'ID' })]; + + await expect(fieldService.createMany(dto)).rejects.toThrow( + new BadRequestException('name is rejected'), + ); + }); + + it('creating many fields fails with reserved field key', async () => { + const channelId = faker.number.int(); + const dto = new CreateManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [createFieldDto({ key: 'id' })]; + + await expect(fieldService.createMany(dto)).rejects.toThrow( + new BadRequestException('key is rejected'), + ); + }); }); describe('replaceMany', () => { it('replacing many fields succeeds with valid inputs', async () => { @@ -142,6 +393,50 @@ describe('FieldService suite', () => { updatingFieldDtos.length + creatingFieldDtos.length, ); }); + + it('replacing many fields with OpenSearch enabled should call putMappings', async () => { + const channelId = faker.number.int(); + const dto = new ReplaceManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [updateFieldDto({})]; + + const mockFields = [createFieldDto({})] as FieldEntity[]; + jest + .spyOn(fieldMySQLService, 'replaceMany') + .mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(true); + jest + .spyOn(osRepository, 'putMappings') + .mockResolvedValue({} as Indices_PutMapping_Response); + + await fieldService.replaceMany(dto); + + expect(osRepository.putMappings).toHaveBeenCalledWith({ + index: channelId.toString(), + + mappings: expect.any(Object), + }); + }); + + it('replacing many fields with OpenSearch disabled should not call putMappings', async () => { + const channelId = faker.number.int(); + const dto = new ReplaceManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [updateFieldDto({})]; + + const mockFields = [createFieldDto({})] as FieldEntity[]; + jest + .spyOn(fieldMySQLService, 'replaceMany') + .mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(false); + jest + .spyOn(osRepository, 'putMappings') + .mockResolvedValue({} as Indices_PutMapping_Response); + + await fieldService.replaceMany(dto); + + expect(osRepository.putMappings).not.toHaveBeenCalled(); + }); it('replacing many fields fails with duplicate names', async () => { const channelId = faker.number.int(); const updatingFieldDtos = Array.from({ @@ -264,5 +559,216 @@ describe('FieldService suite', () => { new BadRequestException('field key cannot be changed'), ); }); + + it('replacing many fields fails with invalid field key containing special characters', async () => { + const channelId = faker.number.int(); + const creatingFieldDtos = [createFieldDto({ key: 'invalid-key!' })]; + const dto = new ReplaceManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [...creatingFieldDtos]; + jest.spyOn(fieldRepo, 'findBy').mockResolvedValue([]); + jest.spyOn(optionRepo, 'find').mockResolvedValue([]); + + await expect(fieldService.replaceMany(dto)).rejects.toThrow( + new BadRequestException( + 'field key only should contain alphanumeric and underscore', + ), + ); + }); + + it('replacing many fields fails with reserved field name in creating fields', async () => { + const channelId = faker.number.int(); + const creatingFieldDtos = [createFieldDto({ name: 'ID' })]; + const dto = new ReplaceManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [...creatingFieldDtos]; + jest.spyOn(fieldRepo, 'findBy').mockResolvedValue([]); + jest.spyOn(optionRepo, 'find').mockResolvedValue([]); + + await expect(fieldService.replaceMany(dto)).rejects.toThrow( + new BadRequestException('name is rejected'), + ); + }); + + it('replacing many fields fails with reserved field key in creating fields', async () => { + const channelId = faker.number.int(); + const creatingFieldDtos = [createFieldDto({ key: 'id' })]; + const dto = new ReplaceManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [...creatingFieldDtos]; + jest.spyOn(fieldRepo, 'findBy').mockResolvedValue([]); + jest.spyOn(optionRepo, 'find').mockResolvedValue([]); + + await expect(fieldService.replaceMany(dto)).rejects.toThrow( + new BadRequestException('key is rejected'), + ); + }); + }); + + describe('findByChannelId', () => { + it('should return fields for given channel id', async () => { + const channelId = faker.number.int(); + const mockFields = [createFieldDto(), createFieldDto()] as FieldEntity[]; + + jest + .spyOn(fieldMySQLService, 'findByChannelId') + .mockResolvedValue(mockFields); + + const result = await fieldService.findByChannelId({ channelId }); + + expect(fieldMySQLService.findByChannelId).toHaveBeenCalledWith({ + channelId, + }); + expect(result).toEqual(mockFields); + }); + + it('should return empty array when no fields found', async () => { + const channelId = faker.number.int(); + + jest.spyOn(fieldMySQLService, 'findByChannelId').mockResolvedValue([]); + + const result = await fieldService.findByChannelId({ channelId }); + + expect(result).toEqual([]); + }); + }); + + describe('findByIds', () => { + it('should return fields for given ids', async () => { + const ids = [faker.number.int(), faker.number.int()]; + const mockFields = [createFieldDto(), createFieldDto()] as FieldEntity[]; + + jest.spyOn(fieldMySQLService, 'findByIds').mockResolvedValue(mockFields); + + const result = await fieldService.findByIds(ids); + + expect(fieldMySQLService.findByIds).toHaveBeenCalledWith(ids); + expect(result).toEqual(mockFields); + }); + + it('should return empty array when no fields found', async () => { + const ids = [faker.number.int()]; + + jest.spyOn(fieldMySQLService, 'findByIds').mockResolvedValue([]); + + const result = await fieldService.findByIds(ids); + + expect(result).toEqual([]); + }); + + it('should handle empty ids array', async () => { + jest.spyOn(fieldMySQLService, 'findByIds').mockResolvedValue([]); + + const result = await fieldService.findByIds([]); + + expect(fieldMySQLService.findByIds).toHaveBeenCalledWith([]); + expect(result).toEqual([]); + }); + }); + + describe('edge cases', () => { + it('createMany should handle empty fields array', async () => { + const channelId = faker.number.int(); + const dto = new CreateManyFieldsDto(); + dto.channelId = channelId; + dto.fields = []; + + const mockFields = [] as FieldEntity[]; + jest.spyOn(fieldMySQLService, 'createMany').mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(false); + + const result = await fieldService.createMany(dto); + + expect(result).toEqual([]); + expect(fieldMySQLService.createMany).toHaveBeenCalledWith(dto); + }); + + it('replaceMany should handle empty fields array', async () => { + const channelId = faker.number.int(); + const dto = new ReplaceManyFieldsDto(); + dto.channelId = channelId; + dto.fields = []; + + const mockFields = [] as FieldEntity[]; + jest + .spyOn(fieldMySQLService, 'replaceMany') + .mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(false); + + await fieldService.replaceMany(dto); + + expect(fieldMySQLService.replaceMany).toHaveBeenCalledWith(dto); + }); + + it('createMany should handle null channelId', async () => { + const dto = new CreateManyFieldsDto(); + dto.channelId = null as unknown as number; + dto.fields = [createFieldDto()]; + + const mockFields = [createFieldDto({})] as FieldEntity[]; + jest.spyOn(fieldMySQLService, 'createMany').mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(false); + + const result = await fieldService.createMany(dto); + + expect(result).toEqual(mockFields); + }); + + it('fieldsToMapping should handle fields with null/undefined properties', () => { + const fields = [ + { + key: 'testField', + format: FieldFormatEnum.text, + } as FieldEntity, + { + key: '', + format: FieldFormatEnum.keyword, + } as FieldEntity, + ]; + + const mapping = fieldService.fieldsToMapping(fields); + + expect(mapping.testField).toBeDefined(); + expect(mapping['']).toBeDefined(); + }); + + it('createMany should handle fields with empty options array', async () => { + const channelId = faker.number.int(); + const dto = new CreateManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [ + createFieldDto({ + format: FieldFormatEnum.select, + options: [], + }), + ]; + + const mockFields = [createFieldDto({})] as FieldEntity[]; + jest.spyOn(fieldMySQLService, 'createMany').mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(false); + + const result = await fieldService.createMany(dto); + + expect(result).toEqual(mockFields); + }); + + it('replaceMany should handle mixed creating and updating fields', async () => { + const channelId = faker.number.int(); + const creatingFieldDtos = [createFieldDto({})]; + const updatingFieldDtos = [updateFieldDto({})]; + const dto = new ReplaceManyFieldsDto(); + dto.channelId = channelId; + dto.fields = [...creatingFieldDtos, ...updatingFieldDtos]; + + const mockFields = [createFieldDto({})] as FieldEntity[]; + jest + .spyOn(fieldMySQLService, 'replaceMany') + .mockResolvedValue(mockFields); + jest.spyOn(configService, 'get').mockReturnValue(false); + + await fieldService.replaceMany(dto); + + expect(fieldMySQLService.replaceMany).toHaveBeenCalledWith(dto); + }); }); }); diff --git a/apps/api/src/domains/admin/channel/option/option.controller.spec.ts b/apps/api/src/domains/admin/channel/option/option.controller.spec.ts index 092541597..22f293659 100644 --- a/apps/api/src/domains/admin/channel/option/option.controller.spec.ts +++ b/apps/api/src/domains/admin/channel/option/option.controller.spec.ts @@ -19,8 +19,12 @@ import { DataSource } from 'typeorm'; import { getMockProvider, MockDataSource } from '@/test-utils/util-functions'; import { CreateOptionRequestDto } from './dtos/requests'; +import { + OptionKeyDuplicatedException, + OptionNameDuplicatedException, +} from './exceptions'; import { OptionController } from './option.controller'; -import { OptionEntity } from './option.entity'; +import type { OptionEntity } from './option.entity'; import { OptionService } from './option.service'; const MockSelectOptionService = { @@ -43,20 +47,250 @@ describe('SelectOptionController', () => { optionController = module.get(OptionController); }); - it('getOptions', async () => { - const options = [new OptionEntity()]; - jest - .spyOn(MockSelectOptionService, 'findByFieldId') - .mockReturnValue(options); - const fieldId = faker.number.int(); - await optionController.getOptions(fieldId); - expect(MockSelectOptionService.findByFieldId).toHaveBeenCalledTimes(1); + describe('getOptions', () => { + it('should return transformed options for valid fieldId', async () => { + const fieldId = faker.number.int(); + const mockOptions = [ + { id: 1, name: 'Option 1', key: 'option1', fieldId }, + { id: 2, name: 'Option 2', key: 'option2', fieldId }, + ] as unknown as OptionEntity[]; + + jest + .spyOn(MockSelectOptionService, 'findByFieldId') + .mockResolvedValue(mockOptions); + + const result = await optionController.getOptions(fieldId); + + expect(MockSelectOptionService.findByFieldId).toHaveBeenCalledWith({ + fieldId, + }); + expect(MockSelectOptionService.findByFieldId).toHaveBeenCalledTimes(1); + expect(result).toHaveLength(2); + expect(result[0]).toHaveProperty('id', 1); + expect(result[0]).toHaveProperty('name', 'Option 1'); + expect(result[0]).toHaveProperty('key', 'option1'); + }); + + it('should return empty array when no options found', async () => { + const fieldId = faker.number.int(); + + jest + .spyOn(MockSelectOptionService, 'findByFieldId') + .mockResolvedValue([]); + + const result = await optionController.getOptions(fieldId); + + expect(result).toEqual([]); + expect(MockSelectOptionService.findByFieldId).toHaveBeenCalledWith({ + fieldId, + }); + }); + + it('should handle service errors', async () => { + const fieldId = faker.number.int(); + const error = new Error('Database connection failed'); + + jest + .spyOn(MockSelectOptionService, 'findByFieldId') + .mockRejectedValue(error); + + await expect(optionController.getOptions(fieldId)).rejects.toThrow(error); + }); + }); + describe('createOption', () => { + it('should create option successfully with valid data', async () => { + const fieldId = faker.number.int(); + const dto = new CreateOptionRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.key = faker.string.alphanumeric(10); + + const mockCreatedOption = { + id: faker.number.int(), + name: dto.name, + key: dto.key, + fieldId, + } as unknown as OptionEntity; + + jest + .spyOn(MockSelectOptionService, 'create') + .mockResolvedValue(mockCreatedOption); + + const result = await optionController.createOption(fieldId, dto); + + expect(MockSelectOptionService.create).toHaveBeenCalledWith({ + fieldId, + name: dto.name, + key: dto.key, + }); + expect(MockSelectOptionService.create).toHaveBeenCalledTimes(1); + expect(result).toHaveProperty('id', mockCreatedOption.id); + }); + + it('should throw OptionNameDuplicatedException when name is duplicated', async () => { + const fieldId = faker.number.int(); + const dto = new CreateOptionRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.key = faker.string.alphanumeric(10); + + jest + .spyOn(MockSelectOptionService, 'create') + .mockRejectedValue(new OptionNameDuplicatedException()); + + await expect(optionController.createOption(fieldId, dto)).rejects.toThrow( + OptionNameDuplicatedException, + ); + }); + + it('should throw OptionKeyDuplicatedException when key is duplicated', async () => { + const fieldId = faker.number.int(); + const dto = new CreateOptionRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.key = faker.string.alphanumeric(10); + + jest + .spyOn(MockSelectOptionService, 'create') + .mockRejectedValue(new OptionKeyDuplicatedException()); + + await expect(optionController.createOption(fieldId, dto)).rejects.toThrow( + OptionKeyDuplicatedException, + ); + }); + + it('should handle service errors', async () => { + const fieldId = faker.number.int(); + const dto = new CreateOptionRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.key = faker.string.alphanumeric(10); + + const error = new Error('Database connection failed'); + jest.spyOn(MockSelectOptionService, 'create').mockRejectedValue(error); + + await expect(optionController.createOption(fieldId, dto)).rejects.toThrow( + error, + ); + }); + + it('should handle empty name and key', async () => { + const fieldId = faker.number.int(); + const dto = new CreateOptionRequestDto(); + dto.name = ''; + dto.key = ''; + + const mockCreatedOption = { + id: faker.number.int(), + name: '', + key: '', + fieldId, + } as unknown as OptionEntity; + + jest + .spyOn(MockSelectOptionService, 'create') + .mockResolvedValue(mockCreatedOption); + + const result = await optionController.createOption(fieldId, dto); + + expect(result).toHaveProperty('id', mockCreatedOption.id); + }); + }); + + describe('parameter validation', () => { + it('should handle invalid fieldId parameter', async () => { + const invalidFieldId = 'invalid' as unknown as number; + + await expect( + optionController.getOptions(invalidFieldId), + ).rejects.toThrow(); + }); + + it('should handle negative fieldId', async () => { + const negativeFieldId = -1; + + jest + .spyOn(MockSelectOptionService, 'findByFieldId') + .mockResolvedValue([]); + + const result = await optionController.getOptions(negativeFieldId); + + expect(MockSelectOptionService.findByFieldId).toHaveBeenCalledWith({ + fieldId: negativeFieldId, + }); + expect(result).toEqual([]); + }); + + it('should handle zero fieldId', async () => { + const zeroFieldId = 0; + + jest + .spyOn(MockSelectOptionService, 'findByFieldId') + .mockResolvedValue([]); + + const result = await optionController.getOptions(zeroFieldId); + + expect(MockSelectOptionService.findByFieldId).toHaveBeenCalledWith({ + fieldId: zeroFieldId, + }); + expect(result).toEqual([]); + }); }); - it('creaetOption', async () => { - const fieldId = faker.number.int(); - const dto = new CreateOptionRequestDto(); - dto.name = faker.string.sample(); - await optionController.createOption(fieldId, dto); - expect(MockSelectOptionService.create).toHaveBeenCalledTimes(1); + + describe('edge cases', () => { + it('should handle very large fieldId', async () => { + const largeFieldId = Number.MAX_SAFE_INTEGER; + + jest + .spyOn(MockSelectOptionService, 'findByFieldId') + .mockResolvedValue([]); + + const result = await optionController.getOptions(largeFieldId); + + expect(MockSelectOptionService.findByFieldId).toHaveBeenCalledWith({ + fieldId: largeFieldId, + }); + expect(result).toEqual([]); + }); + + it('should handle null DTO properties', async () => { + const fieldId = faker.number.int(); + const dto = new CreateOptionRequestDto(); + dto.name = null as unknown as string; + dto.key = null as unknown as string; + + const mockCreatedOption = { + id: faker.number.int(), + name: null, + key: null, + fieldId, + } as unknown as OptionEntity; + + jest + .spyOn(MockSelectOptionService, 'create') + .mockResolvedValue(mockCreatedOption); + + const result = await optionController.createOption(fieldId, dto); + + expect(result).toHaveProperty('id', mockCreatedOption.id); + }); + + it('should handle undefined DTO properties', async () => { + const fieldId = faker.number.int(); + const dto = new CreateOptionRequestDto(); + dto.name = undefined as unknown as string; + dto.key = undefined as unknown as string; + + const mockCreatedOption = { + id: faker.number.int(), + name: undefined, + key: undefined, + fieldId, + } as unknown as OptionEntity; + + jest + .spyOn(MockSelectOptionService, 'create') + .mockResolvedValue(mockCreatedOption); + + const result = await optionController.createOption(fieldId, dto); + + expect(result).toHaveProperty('id', mockCreatedOption.id); + }); }); }); diff --git a/apps/api/src/domains/admin/channel/option/option.service.spec.ts b/apps/api/src/domains/admin/channel/option/option.service.spec.ts index 43f269a94..570b144a1 100644 --- a/apps/api/src/domains/admin/channel/option/option.service.spec.ts +++ b/apps/api/src/domains/admin/channel/option/option.service.spec.ts @@ -196,5 +196,261 @@ describe('Option Test suite', () => { expect(optionRepo.save).toHaveBeenCalledTimes(length); }); + + it('replacing many options fails with duplicate names', async () => { + const fieldId = faker.number.int(); + const dto = new ReplaceManyOptionsDto(); + dto.fieldId = fieldId; + dto.options = Array.from({ + length: faker.number.int({ min: 2, max: 10 }), + }).map(() => ({ + id: faker.number.int(), + key: faker.string.sample(), + name: 'duplicateName', + })); + + await expect(optionService.replaceMany(dto)).rejects.toThrow( + OptionNameDuplicatedException, + ); + }); + + it('replacing many options fails with duplicate keys', async () => { + const fieldId = faker.number.int(); + const dto = new ReplaceManyOptionsDto(); + dto.fieldId = fieldId; + dto.options = Array.from({ + length: faker.number.int({ min: 2, max: 10 }), + }).map(() => ({ + id: faker.number.int(), + key: 'duplicateKey', + name: faker.string.sample(), + })); + + await expect(optionService.replaceMany(dto)).rejects.toThrow( + OptionKeyDuplicatedException, + ); + }); + + it('replacing many options succeeds with empty options array', async () => { + const fieldId = faker.number.int(); + const dto = new ReplaceManyOptionsDto(); + dto.fieldId = fieldId; + dto.options = []; + jest.spyOn(optionRepo, 'find').mockResolvedValue([]); + jest.spyOn(optionRepo, 'query'); + jest.spyOn(optionRepo, 'save'); + + await optionService.replaceMany(dto); + + expect(optionRepo.save).toHaveBeenCalledTimes(0); + }); + + it('replacing many options handles inactive options correctly', async () => { + const fieldId = faker.number.int(); + const optionId = faker.number.int(); + const key = faker.string.sample(); + const name = faker.string.sample(); + const dto = new ReplaceManyOptionsDto(); + dto.fieldId = fieldId; + dto.options = [{ id: optionId, key, name }]; + + jest.spyOn(optionRepo, 'find').mockResolvedValue([ + { + id: optionId, + key: 'deleted_' + key, + name, + deletedAt: new Date(), + }, + ] as unknown as OptionEntity[]); + jest.spyOn(optionRepo, 'query'); + jest.spyOn(optionRepo, 'save'); + + await optionService.replaceMany(dto); + + expect(optionRepo.save).toHaveBeenCalledTimes(1); + }); + + it('replacing many options deletes unused options', async () => { + const fieldId = faker.number.int(); + const existingOptionId = faker.number.int(); + const dto = new ReplaceManyOptionsDto(); + dto.fieldId = fieldId; + dto.options = [ + { + id: faker.number.int(), + key: faker.string.sample(), + name: faker.string.sample(), + }, + ]; + + jest.spyOn(optionRepo, 'find').mockResolvedValue([ + { + id: existingOptionId, + key: faker.string.sample(), + name: faker.string.sample(), + deletedAt: null, + }, + ] as unknown as OptionEntity[]); + jest.spyOn(optionRepo, 'query'); + jest.spyOn(optionRepo, 'save'); + + await optionService.replaceMany(dto); + + expect(optionRepo.query).toHaveBeenCalledWith( + expect.stringContaining('UPDATE'), + expect.arrayContaining([expect.any(String), [existingOptionId]]), + ); + }); + }); + + describe('findByFieldId', () => { + it('finding options by field id succeeds', async () => { + const fieldId = faker.number.int(); + const mockOptions = Array.from({ + length: faker.number.int({ min: 1, max: 10 }), + }).map(() => ({ + id: faker.number.int(), + key: faker.string.sample(), + name: faker.string.sample(), + fieldId, + })) as unknown as OptionEntity[]; + + jest.spyOn(optionRepo, 'findBy').mockResolvedValue(mockOptions); + + const result = await optionService.findByFieldId({ fieldId }); + + expect(optionRepo.findBy).toHaveBeenCalledWith({ + field: { id: fieldId }, + }); + expect(result).toEqual(mockOptions); + }); + + it('finding options by field id returns empty array when no options exist', async () => { + const fieldId = faker.number.int(); + jest.spyOn(optionRepo, 'findBy').mockResolvedValue([]); + + const result = await optionService.findByFieldId({ fieldId }); + + expect(result).toEqual([]); + }); + }); + + describe('create edge cases', () => { + it('creating an option with empty key succeeds', async () => { + const fieldId = faker.number.int(); + const name = faker.string.sample(); + const dto = new CreateOptionDto(); + dto.fieldId = fieldId; + dto.key = ''; + dto.name = name; + jest.spyOn(optionRepo, 'findBy').mockResolvedValue([]); + jest + .spyOn(optionRepo, 'save') + .mockImplementation(() => Promise.resolve({ key: '', name } as any)); + + const result = await optionService.create(dto); + + expect(result.key).toBe(''); + }); + + it('creating an option with empty name succeeds', async () => { + const fieldId = faker.number.int(); + const key = faker.string.sample(); + const dto = new CreateOptionDto(); + dto.fieldId = fieldId; + dto.key = key; + dto.name = ''; + jest.spyOn(optionRepo, 'findBy').mockResolvedValue([]); + jest + .spyOn(optionRepo, 'save') + .mockImplementation(() => Promise.resolve({ key, name: '' } as any)); + + const result = await optionService.create(dto); + + expect(result.name).toBe(''); + }); + + it('creating an option with null fieldId succeeds', async () => { + const dto = new CreateOptionDto(); + + dto.fieldId = null as any; + dto.key = faker.string.sample(); + dto.name = faker.string.sample(); + jest.spyOn(optionRepo, 'findBy').mockResolvedValue([]); + jest + .spyOn(optionRepo, 'save') + .mockImplementation(() => + Promise.resolve({ key: '', name: faker.string.sample() } as any), + ); + + const result = await optionService.create(dto); + + expect(result).toBeDefined(); + }); + }); + + describe('createMany edge cases', () => { + it('creating many options with empty array succeeds', async () => { + const fieldId = faker.number.int(); + const dto = new CreateManyOptionsDto(); + dto.fieldId = fieldId; + dto.options = []; + jest + .spyOn(optionRepo, 'save') + .mockImplementation(() => Promise.resolve([] as any)); + + const result = await optionService.createMany(dto); + + expect(result).toEqual([]); + expect(optionRepo.save).toHaveBeenCalledWith([]); + }); + + it('creating many options with single option succeeds', async () => { + const fieldId = faker.number.int(); + const dto = new CreateManyOptionsDto(); + dto.fieldId = fieldId; + dto.options = [ + { key: faker.string.sample(), name: faker.string.sample() }, + ]; + jest + .spyOn(optionRepo, 'save') + .mockImplementation(() => Promise.resolve([{}] as any)); + + const result = await optionService.createMany(dto); + + expect(result).toHaveLength(1); + }); + }); + + describe('replaceMany edge cases', () => { + it('replacing many options with null options array succeeds', async () => { + const fieldId = faker.number.int(); + const dto = new ReplaceManyOptionsDto(); + dto.fieldId = fieldId; + + dto.options = null as any; + jest.spyOn(optionRepo, 'find').mockResolvedValue([]); + jest.spyOn(optionRepo, 'query'); + jest.spyOn(optionRepo, 'save'); + + await optionService.replaceMany(dto); + + expect(optionRepo.save).toHaveBeenCalledTimes(0); + }); + + it('replacing many options with undefined options array succeeds', async () => { + const fieldId = faker.number.int(); + const dto = new ReplaceManyOptionsDto(); + dto.fieldId = fieldId; + + dto.options = undefined as any; + jest.spyOn(optionRepo, 'find').mockResolvedValue([]); + jest.spyOn(optionRepo, 'query'); + jest.spyOn(optionRepo, 'save'); + + await optionService.replaceMany(dto); + + expect(optionRepo.save).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/apps/api/src/domains/admin/feedback/feedback.controller.spec.ts b/apps/api/src/domains/admin/feedback/feedback.controller.spec.ts index 5550f9a1c..747b9d3cc 100644 --- a/apps/api/src/domains/admin/feedback/feedback.controller.spec.ts +++ b/apps/api/src/domains/admin/feedback/feedback.controller.spec.ts @@ -14,6 +14,7 @@ * under the License. */ import { faker } from '@faker-js/faker'; +import { BadRequestException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import type { FastifyReply } from 'fastify'; import { DataSource } from 'typeorm'; @@ -40,6 +41,8 @@ const MockFeedbackService = { updateFeedback: jest.fn(), deleteByIds: jest.fn(), generateFile: jest.fn(), + addIssue: jest.fn(), + removeIssue: jest.fn(), }; const MockAuthService = { validateApiKey: jest.fn(), @@ -55,6 +58,9 @@ describe('FeedbackController', () => { let feedbackController: FeedbackController; beforeEach(async () => { + // Reset all mocks before each test + jest.clearAllMocks(); + const module = await Test.createTestingModule({ controllers: [FeedbackController], providers: [ @@ -69,79 +75,342 @@ describe('FeedbackController', () => { feedbackController = module.get(FeedbackController); }); - it('create', async () => { - const projectId = faker.number.int(); - const channelId = faker.number.int(); - jest - .spyOn(MockFeedbackService, 'create') - .mockResolvedValue({ id: faker.number.int() }); - jest.spyOn(MockChannelService, 'findById').mockResolvedValue({ - project: { id: projectId }, - } as ChannelEntity); - - await feedbackController.create(projectId, channelId, {}); - expect(MockFeedbackService.create).toHaveBeenCalledTimes(1); + describe('create', () => { + it('should create feedback successfully', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const body = { message: faker.string.sample() }; + + jest + .spyOn(MockFeedbackService, 'create') + .mockResolvedValue({ id: feedbackId }); + jest.spyOn(MockChannelService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as ChannelEntity); + + const result = await feedbackController.create( + projectId, + channelId, + body, + ); + + expect(MockChannelService.findById).toHaveBeenCalledWith({ channelId }); + expect(MockFeedbackService.create).toHaveBeenCalledWith({ + data: body, + channelId, + }); + expect(result).toEqual({ id: feedbackId }); + }); + + it('should throw BadRequestException when channel project id does not match', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const differentProjectId = faker.number.int(); + + jest.spyOn(MockChannelService, 'findById').mockResolvedValue({ + project: { id: differentProjectId }, + } as ChannelEntity); + + await expect( + feedbackController.create(projectId, channelId, {}), + ).rejects.toThrow('Invalid channel id'); + + expect(MockChannelService.findById).toHaveBeenCalledWith({ channelId }); + expect(MockFeedbackService.create).not.toHaveBeenCalled(); + }); }); - it('findByChannelId', async () => { - const channelId = faker.number.int(); - const dto = new FindFeedbacksByChannelIdRequestDto( - faker.number.int(), - faker.number.int(), - {}, - ); + describe('findByChannelId', () => { + it('should find feedbacks by channel id successfully', async () => { + const channelId = faker.number.int(); + const limit = faker.number.int({ min: 1, max: 100 }); + const page = faker.number.int({ min: 1, max: 10 }); + + const dto = new FindFeedbacksByChannelIdRequestDto(limit, page, { + message: 'test', + }); + const mockResult = { + items: [{ id: faker.number.int(), message: 'test' }], + meta: { + totalItems: 1, + itemCount: 1, + itemsPerPage: limit, + totalPages: 1, + currentPage: page, + }, + }; + + jest + .spyOn(MockFeedbackService, 'findByChannelIdV2') + .mockResolvedValue(mockResult); + + const result = await feedbackController.findByChannelId(channelId, dto); - await feedbackController.findByChannelId(channelId, dto); - expect(MockFeedbackService.findByChannelIdV2).toHaveBeenCalledTimes(1); + expect(MockFeedbackService.findByChannelIdV2).toHaveBeenCalledWith({ + ...dto, + channelId, + }); + expect(result).toBeDefined(); + }); }); - it('exportFeedbacks', async () => { - const projectId = faker.number.int(); - const channelId = faker.number.int(); - const response = { - type: jest.fn(), - header: jest.fn(), - send: jest.fn(), - } as unknown as FastifyReply; - const dto = new ExportFeedbacksRequestDto( - faker.number.int(), - faker.number.int(), - ); - const userDto = new UserDto(); - jest.spyOn(MockFeedbackService, 'generateFile').mockResolvedValue({ - streamableFile: { getStream: jest.fn() }, - feedbackIds: [], + + describe('addIssue', () => { + it('should add issue to feedback successfully', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const issueId = faker.number.int(); + const mockResult = { success: true }; + + jest.spyOn(MockFeedbackService, 'addIssue').mockResolvedValue(mockResult); + + const result = await feedbackController.addIssue( + channelId, + feedbackId, + issueId, + ); + + expect(MockFeedbackService.addIssue).toHaveBeenCalledWith({ + issueId, + channelId, + feedbackId, + }); + expect(result).toEqual(mockResult); + }); + }); + + describe('removeIssue', () => { + it('should remove issue from feedback successfully', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const issueId = faker.number.int(); + const mockResult = { success: true }; + + jest + .spyOn(MockFeedbackService, 'removeIssue') + .mockResolvedValue(mockResult); + + const result = await feedbackController.removeIssue( + channelId, + feedbackId, + issueId, + ); + + expect(MockFeedbackService.removeIssue).toHaveBeenCalledWith({ + issueId, + channelId, + feedbackId, + }); + expect(result).toEqual(mockResult); + }); + }); + + describe('exportFeedbacks', () => { + it('should export feedbacks successfully', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const response = { + type: jest.fn(), + header: jest.fn(), + send: jest.fn(), + } as unknown as FastifyReply; + const dto = new ExportFeedbacksRequestDto( + faker.number.int(), + faker.number.int(), + ); + const userDto = new UserDto(); + const mockStream = { pipe: jest.fn() }; + + jest.spyOn(MockFeedbackService, 'generateFile').mockResolvedValue({ + streamableFile: { getStream: jest.fn().mockReturnValue(mockStream) }, + feedbackIds: [faker.number.int()], + }); + jest.spyOn(MockChannelService, 'findById').mockResolvedValue({ + project: { name: faker.string.sample() }, + name: faker.string.sample(), + } as ChannelEntity); + + await feedbackController.exportFeedbacks( + projectId, + channelId, + dto, + response, + userDto, + ); + + expect(MockChannelService.findById).toHaveBeenCalledWith({ channelId }); + expect(MockFeedbackService.generateFile).toHaveBeenCalledWith({ + projectId, + channelId, + queries: dto.queries, + operator: dto.operator, + sort: dto.sort, + type: dto.type, + fieldIds: dto.fieldIds, + filterFeedbackIds: dto.filterFeedbackIds, + defaultQueries: dto.defaultQueries, + }); + expect(MockHistoryService.createHistory).toHaveBeenCalled(); + }); + }); + + describe('updateFeedback', () => { + it('should update feedback successfully', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const body = { message: faker.string.sample() }; + + jest + .spyOn(MockFeedbackService, 'updateFeedback') + .mockResolvedValue(undefined); + + await feedbackController.updateFeedback(channelId, feedbackId, body); + + expect(MockFeedbackService.updateFeedback).toHaveBeenCalledWith({ + channelId, + feedbackId, + data: body, + }); }); - jest.spyOn(MockChannelService, 'findById').mockResolvedValue({ - project: { name: faker.string.sample() }, - } as ChannelEntity); - - await feedbackController.exportFeedbacks( - projectId, - channelId, - dto, - response, - userDto, - ); - - expect(MockFeedbackService.generateFile).toHaveBeenCalledTimes(1); }); - it('updateFeedback', async () => { - const channelId = faker.number.int(); - const feedbackId = faker.number.int(); - const body = { [faker.string.sample()]: faker.string.sample() }; - await feedbackController.updateFeedback(channelId, feedbackId, body); - expect(MockFeedbackService.updateFeedback).toHaveBeenCalledTimes(1); + describe('deleteMany', () => { + it('should delete feedbacks successfully', async () => { + const channelId = faker.number.int(); + const feedbackIds = [faker.number.int(), faker.number.int()]; + + const dto = new DeleteFeedbacksRequestDto(); + dto.feedbackIds = feedbackIds; + + jest + .spyOn(MockFeedbackService, 'deleteByIds') + .mockResolvedValue(undefined); + + await feedbackController.deleteMany(channelId, dto); + + expect(MockFeedbackService.deleteByIds).toHaveBeenCalledWith({ + channelId, + feedbackIds, + }); + }); }); - it('delete Feedback', async () => { - const channelId = faker.number.int(); - const feedbackIds = [faker.number.int()]; + describe('Error Cases', () => { + it('should handle service errors in create', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const body = { message: faker.string.sample() }; - const dto = new DeleteFeedbacksRequestDto(); - dto.feedbackIds = feedbackIds; + jest.spyOn(MockChannelService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as ChannelEntity); + jest + .spyOn(MockFeedbackService, 'create') + .mockRejectedValue(new BadRequestException('Invalid field key: test')); - await feedbackController.deleteMany(channelId, dto); - expect(MockFeedbackService.deleteByIds).toHaveBeenCalledTimes(1); + await expect( + feedbackController.create(projectId, channelId, body), + ).rejects.toThrow('Invalid field key: test'); + }); + + it('should handle service errors in findByChannelId', async () => { + const channelId = faker.number.int(); + const dto = new FindFeedbacksByChannelIdRequestDto(10, 1, {}); + + jest + .spyOn(MockFeedbackService, 'findByChannelIdV2') + .mockRejectedValue(new BadRequestException('Invalid channel')); + + await expect( + feedbackController.findByChannelId(channelId, dto), + ).rejects.toThrow('Invalid channel'); + }); + + it('should handle service errors in updateFeedback', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const body = { message: faker.string.sample() }; + + jest + .spyOn(MockFeedbackService, 'updateFeedback') + .mockRejectedValue(new BadRequestException('This field is read-only')); + + await expect( + feedbackController.updateFeedback(channelId, feedbackId, body), + ).rejects.toThrow('This field is read-only'); + }); + + it('should handle service errors in deleteMany', async () => { + const channelId = faker.number.int(); + const feedbackIds = [faker.number.int()]; + const dto = new DeleteFeedbacksRequestDto(); + dto.feedbackIds = feedbackIds; + + jest + .spyOn(MockFeedbackService, 'deleteByIds') + .mockRejectedValue(new BadRequestException('Feedback not found')); + + await expect( + feedbackController.deleteMany(channelId, dto), + ).rejects.toThrow('Feedback not found'); + }); + + it('should handle service errors in addIssue', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const issueId = faker.number.int(); + + jest + .spyOn(MockFeedbackService, 'addIssue') + .mockRejectedValue(new BadRequestException('Issue not found')); + + await expect( + feedbackController.addIssue(channelId, feedbackId, issueId), + ).rejects.toThrow('Issue not found'); + }); + + it('should handle service errors in removeIssue', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const issueId = faker.number.int(); + + jest + .spyOn(MockFeedbackService, 'removeIssue') + .mockRejectedValue(new BadRequestException('Issue not found')); + + await expect( + feedbackController.removeIssue(channelId, feedbackId, issueId), + ).rejects.toThrow('Issue not found'); + }); + + it('should handle service errors in exportFeedbacks', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + const response = { + type: jest.fn(), + header: jest.fn(), + send: jest.fn(), + } as unknown as FastifyReply; + const dto = new ExportFeedbacksRequestDto(10, 1); + const userDto = new UserDto(); + + jest.spyOn(MockChannelService, 'findById').mockResolvedValue({ + project: { name: faker.string.sample() }, + name: faker.string.sample(), + } as ChannelEntity); + jest + .spyOn(MockFeedbackService, 'generateFile') + .mockRejectedValue(new BadRequestException('Invalid export type')); + + await expect( + feedbackController.exportFeedbacks( + projectId, + channelId, + dto, + response, + userDto, + ), + ).rejects.toThrow('Invalid export type'); + }); }); }); diff --git a/apps/api/src/domains/admin/feedback/feedback.service.spec.ts b/apps/api/src/domains/admin/feedback/feedback.service.spec.ts index 4f694d5fe..b9e7b0002 100644 --- a/apps/api/src/domains/admin/feedback/feedback.service.spec.ts +++ b/apps/api/src/domains/admin/feedback/feedback.service.spec.ts @@ -15,6 +15,8 @@ */ import { faker } from '@faker-js/faker'; import { BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { ClsModule, ClsService } from 'nestjs-cls'; @@ -30,13 +32,30 @@ import type { ChannelRepositoryStub } from '@/test-utils/stubs'; import { createQueryBuilder, TestConfig } from '@/test-utils/util-functions'; import { FeedbackServiceProviders } from '../../../test-utils/providers/feedback.service.providers'; import { ChannelEntity } from '../channel/channel/channel.entity'; +import { ChannelService } from '../channel/channel/channel.service'; import { RESERVED_FIELD_KEYS } from '../channel/field/field.constants'; import { FieldEntity } from '../channel/field/field.entity'; +import { FieldService } from '../channel/field/field.service'; +import { OptionService } from '../channel/option/option.service'; import { IssueEntity } from '../project/issue/issue.entity'; +import { IssueService } from '../project/issue/issue.service'; +import { ProjectService } from '../project/project/project.service'; import { FeedbackIssueStatisticsEntity } from '../statistics/feedback-issue/feedback-issue-statistics.entity'; import { FeedbackStatisticsEntity } from '../statistics/feedback/feedback-statistics.entity'; import { IssueStatisticsEntity } from '../statistics/issue/issue-statistics.entity'; -import { CreateFeedbackDto } from './dtos'; +import { + AddIssueDto, + CountByProjectIdDto, + CreateFeedbackDto, + DeleteByIdsDto, + FindFeedbacksByChannelIdDto, + GenerateExcelDto, + RemoveIssueDto, + UpdateFeedbackDto, +} from './dtos'; +import type { FindFeedbacksByChannelIdDtoV2 } from './dtos/find-feedbacks-by-channel-id-v2.dto'; +import { FeedbackMySQLService } from './feedback.mysql.service'; +import { FeedbackOSService } from './feedback.os.service'; import { FeedbackService } from './feedback.service'; describe('FeedbackService Test Suite', () => { @@ -48,7 +67,28 @@ describe('FeedbackService Test Suite', () => { let feedbackStatsRepo: Repository; let issueStatsRepo: Repository; let feedbackIssueStatsRepo: Repository; + let feedbackMySQLService: FeedbackMySQLService; + let feedbackOSService: FeedbackOSService; + let fieldService: FieldService; + let issueService: IssueService; + let _optionService: OptionService; + let channelService: ChannelService; + let projectService: ProjectService; + let configService: ConfigService; + let eventEmitter: EventEmitter2; + beforeEach(async () => { + // Clear all mocks to ensure test isolation + jest.clearAllMocks(); + + // Set default configService mock to disable OpenSearch + jest + .spyOn(ConfigService.prototype, 'get') + .mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + const module = await Test.createTestingModule({ imports: [TestConfig, ClsModule.forFeature()], providers: FeedbackServiceProviders, @@ -66,18 +106,32 @@ describe('FeedbackService Test Suite', () => { feedbackIssueStatsRepo = module.get( getRepositoryToken(FeedbackIssueStatisticsEntity), ); + feedbackMySQLService = + module.get(FeedbackMySQLService); + feedbackOSService = module.get(FeedbackOSService); + fieldService = module.get(FieldService); + issueService = module.get(IssueService); + _optionService = module.get(OptionService); + channelService = module.get(ChannelService); + projectService = module.get(ProjectService); + configService = module.get(ConfigService); + eventEmitter = module.get(EventEmitter2); }); describe('create', () => { beforeEach(() => { + // Clear mocks for each test to ensure isolation + jest.clearAllMocks(); + channelRepo.setImageConfig({ domainWhiteList: ['example.com'], }); }); it('creating a feedback succeeds with valid inputs', async () => { const dto = new CreateFeedbackDto(); - dto.channelId = faker.number.int(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); // Limit range for stability dto.data = JSON.parse(JSON.stringify(feedbackDataFixture)) as object; + jest .spyOn(feedbackStatsRepo, 'findOne') .mockResolvedValue({ count: 1 } as FeedbackStatisticsEntity); @@ -88,8 +142,9 @@ describe('FeedbackService Test Suite', () => { }); it('creating a feedback fails with an invalid channel', async () => { const dto = new CreateFeedbackDto(); - dto.channelId = faker.number.int(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); dto.data = JSON.parse(JSON.stringify(feedbackDataFixture)) as object; + jest.spyOn(fieldRepo, 'find').mockResolvedValue([]); await expect(feedbackService.create(dto)).rejects.toThrow( @@ -155,16 +210,20 @@ describe('FeedbackService Test Suite', () => { ]; for (const { format, invalidValues } of formats) { for (const invalidValue of invalidValues) { + // Clear mocks for each test iteration + jest.clearAllMocks(); + const field = createFieldDto({ format, property: FieldPropertyEnum.EDITABLE, status: FieldStatusEnum.ACTIVE, }); const dto = new CreateFeedbackDto(); - dto.channelId = faker.number.int(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); dto.data = { [field.key]: invalidValue, }; + const spy = jest .spyOn(fieldRepo, 'find') .mockResolvedValue([field] as FieldEntity[]); @@ -177,18 +236,19 @@ describe('FeedbackService Test Suite', () => { ), ); - spy.mockClear(); + spy.mockRestore(); // Use mockRestore instead of mockClear } } }); it('creating a feedback succeeds with valid inputs and issue names', async () => { const dto = new CreateFeedbackDto(); - dto.channelId = faker.number.int(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); dto.data = JSON.parse(JSON.stringify(feedbackDataFixture)) as object; - const issueNames = Array.from({ - length: faker.number.int({ min: 1, max: 1 }), - }).map(() => faker.string.sample()); - dto.data.issueNames = [...issueNames, faker.string.sample()]; + + // Use stable test data + const issueNames = ['test-issue-1', 'test-issue-2']; + dto.data.issueNames = [...issueNames, 'additional-issue']; + jest.spyOn(issueRepo, 'findOneBy').mockResolvedValue(null); jest .spyOn(feedbackStatsRepo, 'findOne') @@ -213,12 +273,12 @@ describe('FeedbackService Test Suite', () => { }); it('creating a feedback succeeds with valid inputs and an existent issue name', async () => { const dto = new CreateFeedbackDto(); - dto.channelId = faker.number.int(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); dto.data = JSON.parse(JSON.stringify(feedbackDataFixture)) as object; - const issueNames = Array.from({ - length: faker.number.int({ min: 1, max: 1 }), - }).map(() => faker.string.sample()); - dto.data.issueNames = [...issueNames]; + + // Use stable test data + dto.data.issueNames = ['existing-issue-1', 'existing-issue-2']; + jest.spyOn(issueRepo, 'findOneBy').mockResolvedValue(null); jest .spyOn(feedbackStatsRepo, 'findOne') @@ -243,9 +303,12 @@ describe('FeedbackService Test Suite', () => { }); it('creating a feedback succeeds with valid inputs and a nonexistent issue name', async () => { const dto = new CreateFeedbackDto(); - dto.channelId = faker.number.int(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); dto.data = JSON.parse(JSON.stringify(feedbackDataFixture)) as object; - dto.data.issueNames = [faker.string.sample()]; + + // Use stable test data + dto.data.issueNames = ['nonexistent-issue']; + jest.spyOn(issueRepo, 'findOneBy').mockResolvedValue(null); jest .spyOn(feedbackStatsRepo, 'findOne') @@ -268,5 +331,629 @@ describe('FeedbackService Test Suite', () => { expect(feedback.id).toBeDefined(); }); + it('creating a feedback fails with invalid image domain', async () => { + const dto = new CreateFeedbackDto(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); + + // Use stable test data + const fieldKey = 'test-image-field'; + const field = createFieldDto({ + key: fieldKey, + format: FieldFormatEnum.images, + property: FieldPropertyEnum.EDITABLE, + status: FieldStatusEnum.ACTIVE, + }); + dto.data = { [fieldKey]: ['https://invalid-domain.com/image.jpg'] }; + + jest.spyOn(fieldRepo, 'find').mockResolvedValue([field] as FieldEntity[]); + channelRepo.setImageConfig({ + domainWhiteList: ['example.com'], + }); + + await expect(feedbackService.create(dto)).rejects.toThrow( + new BadRequestException( + `invalid domain in image link: invalid-domain.com (fieldKey: ${fieldKey})`, + ), + ); + }); + it('creating a feedback fails with non-array issueNames', async () => { + const dto = new CreateFeedbackDto(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); + dto.data = JSON.parse(JSON.stringify(feedbackDataFixture)) as object; + + // Use stable test data + dto.data.issueNames = 'not-an-array' as unknown as string[]; + + await expect(feedbackService.create(dto)).rejects.toThrow( + new BadRequestException('issueNames must be array'), + ); + }); + it('creating a feedback succeeds with OpenSearch enabled', async () => { + const dto = new CreateFeedbackDto(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); + + // Use stable test data + const fieldKey = 'test-field'; + const field = createFieldDto({ + key: fieldKey, + format: FieldFormatEnum.text, + }); + dto.data = { [fieldKey]: 'test-value' }; + + jest.spyOn(fieldRepo, 'find').mockResolvedValue([field] as FieldEntity[]); + jest.spyOn(feedbackMySQLService, 'create').mockResolvedValue({ + id: faker.number.int({ min: 1, max: 1000 }), + } as any); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return true; + return false; + }); + jest + .spyOn(feedbackOSService, 'create') + .mockResolvedValue({ id: faker.number.int({ min: 1, max: 1000 }) }); + jest.spyOn(eventEmitter, 'emit').mockImplementation(() => true); + + const feedback = await feedbackService.create(dto); + + expect(feedback.id).toBeDefined(); + expect(eventEmitter.emit).toHaveBeenCalled(); + }); + }); + + describe('findByChannelId', () => { + it('should find feedbacks by channel id successfully', async () => { + const channelId = faker.number.int({ min: 1, max: 1000 }); + const dto = new FindFeedbacksByChannelIdDto(); + dto.channelId = channelId; + dto.query = {}; + + const fields = [createFieldDto()]; + const mockFeedbacks = { + items: [{ id: faker.number.int({ min: 1, max: 1000 }), data: {} }], + meta: { + totalItems: 1, + itemCount: 1, + itemsPerPage: 10, + totalPages: 1, + currentPage: 1, + }, + }; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + jest.spyOn(channelService, 'findById').mockResolvedValue({ + feedbackSearchMaxDays: 30, + project: { id: faker.number.int({ min: 1, max: 1000 }) }, + } as unknown as any); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + jest + .spyOn(feedbackMySQLService, 'findByChannelId') + .mockResolvedValue(mockFeedbacks); + jest.spyOn(issueService, 'findIssuesByFeedbackIds').mockResolvedValue({}); + + const result = await feedbackService.findByChannelId(dto); + + expect(result).toEqual(mockFeedbacks); + expect(fieldService.findByChannelId).toHaveBeenCalledWith({ channelId }); + }); + it('should throw error for invalid channel', async () => { + const dto = new FindFeedbacksByChannelIdDto(); + dto.channelId = faker.number.int({ min: 1, max: 1000 }); + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue([] as FieldEntity[]); + + await expect(feedbackService.findByChannelId(dto)).rejects.toThrow( + new BadRequestException('invalid channel'), + ); + }); + it('should handle fieldKey query parameter', async () => { + const channelId = faker.number.int({ min: 1, max: 1000 }); + const fieldKey = 'test-field-key'; + const dto = new FindFeedbacksByChannelIdDto(); + dto.channelId = channelId; + dto.query = { fieldKey }; + + const fields = [createFieldDto({ key: fieldKey })]; + const mockFeedbacks = { + items: [], + meta: { + totalItems: 0, + itemCount: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 1, + }, + }; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + jest.spyOn(channelService, 'findById').mockResolvedValue({ + feedbackSearchMaxDays: 30, + project: { id: faker.number.int({ min: 1, max: 1000 }) }, + } as unknown as any); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + jest + .spyOn(feedbackMySQLService, 'findByChannelId') + .mockResolvedValue(mockFeedbacks); + jest.spyOn(issueService, 'findIssuesByFeedbackIds').mockResolvedValue({}); + + await feedbackService.findByChannelId(dto); + + expect(fieldService.findByChannelId).toHaveBeenCalledWith({ channelId }); + }); + it('should handle issueName query parameter', async () => { + const channelId = faker.number.int({ min: 1, max: 1000 }); + const issueName = 'test-issue-name'; + const issueId = faker.number.int({ min: 1, max: 1000 }); + const dto = new FindFeedbacksByChannelIdDto(); + dto.channelId = channelId; + dto.query = { issueName }; + + const fields = [createFieldDto()]; + const mockIssue = { id: issueId, name: issueName } as unknown as any; + const mockFeedbacks = { + items: [], + meta: { + totalItems: 0, + itemCount: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 1, + }, + }; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + jest.spyOn(issueService, 'findByName').mockResolvedValue(mockIssue); + jest.spyOn(channelService, 'findById').mockResolvedValue({ + feedbackSearchMaxDays: 30, + project: { id: faker.number.int({ min: 1, max: 1000 }) }, + } as unknown as any); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + jest + .spyOn(feedbackMySQLService, 'findByChannelId') + .mockResolvedValue(mockFeedbacks); + jest.spyOn(issueService, 'findIssuesByFeedbackIds').mockResolvedValue({}); + + await feedbackService.findByChannelId(dto); + + expect(issueService.findByName).toHaveBeenCalledWith({ name: issueName }); + }); + }); + + describe('findByChannelIdV2', () => { + it('should find feedbacks by channel id v2 successfully', async () => { + const channelId = faker.number.int({ min: 1, max: 1000 }); + const dto = { + channelId, + queries: [], + defaultQueries: [], + operator: 'AND' as const, + sort: {}, + page: 1, + limit: 10, + } as FindFeedbacksByChannelIdDtoV2; + + const fields = [createFieldDto()]; + const mockFeedbacks = { + items: [{ id: faker.number.int({ min: 1, max: 1000 }), data: {} }], + meta: { + totalItems: 1, + itemCount: 1, + itemsPerPage: 10, + totalPages: 1, + currentPage: 1, + }, + }; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + jest.spyOn(channelService, 'findById').mockResolvedValue({ + feedbackSearchMaxDays: 30, + project: { id: faker.number.int({ min: 1, max: 1000 }) }, + } as unknown as any); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + jest + .spyOn(feedbackMySQLService, 'findByChannelIdV2') + .mockResolvedValue(mockFeedbacks); + jest.spyOn(issueService, 'findIssuesByFeedbackIds').mockResolvedValue({}); + + const result = await feedbackService.findByChannelIdV2(dto); + + expect(result).toEqual(mockFeedbacks); + }); + it('should throw error for invalid channel in v2', async () => { + const dto = { + channelId: faker.number.int(), + queries: [], + defaultQueries: [], + operator: 'AND' as const, + sort: {}, + page: 1, + limit: 10, + } as FindFeedbacksByChannelIdDtoV2; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue([] as FieldEntity[]); + + await expect(feedbackService.findByChannelIdV2(dto)).rejects.toThrow( + new BadRequestException('invalid channel'), + ); + }); + }); + + describe('updateFeedback', () => { + it('should update feedback successfully', async () => { + const fieldKey = faker.string.sample(); + const fieldValue = faker.string.sample(); + const dto = new UpdateFeedbackDto(); + dto.feedbackId = faker.number.int(); + dto.channelId = faker.number.int(); + dto.data = { [fieldKey]: fieldValue }; + + const field = createFieldDto({ + key: fieldKey, + format: FieldFormatEnum.text, + property: FieldPropertyEnum.EDITABLE, + status: FieldStatusEnum.ACTIVE, + }); + const fields = [field]; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + jest + .spyOn(feedbackMySQLService, 'updateFeedback') + .mockResolvedValue(undefined); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + + await feedbackService.updateFeedback(dto); + + expect(fieldService.findByChannelId).toHaveBeenCalledWith({ + channelId: dto.channelId, + }); + expect(feedbackMySQLService.updateFeedback).toHaveBeenCalledWith({ + feedbackId: dto.feedbackId, + data: dto.data, + }); + }); + it('should throw error for invalid field name', async () => { + const dto = new UpdateFeedbackDto(); + dto.feedbackId = faker.number.int(); + dto.channelId = faker.number.int(); + dto.data = { invalidField: faker.string.sample() }; + + const fields = [createFieldDto()]; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + + await expect(feedbackService.updateFeedback(dto)).rejects.toThrow( + new BadRequestException('invalid field name'), + ); + }); + it('should throw error for read-only field', async () => { + const fieldKey = faker.string.sample(); + const dto = new UpdateFeedbackDto(); + dto.feedbackId = faker.number.int(); + dto.channelId = faker.number.int(); + dto.data = { [fieldKey]: faker.string.sample() }; + + const field = createFieldDto({ + key: fieldKey, + property: FieldPropertyEnum.READ_ONLY, + status: FieldStatusEnum.ACTIVE, + }); + const fields = [field]; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + + await expect(feedbackService.updateFeedback(dto)).rejects.toThrow( + new BadRequestException('this field is read-only'), + ); + }); + it('should throw error for inactive field', async () => { + const fieldKey = faker.string.sample(); + const dto = new UpdateFeedbackDto(); + dto.feedbackId = faker.number.int(); + dto.channelId = faker.number.int(); + dto.data = { [fieldKey]: faker.string.sample() }; + + const field = createFieldDto({ + key: fieldKey, + property: FieldPropertyEnum.EDITABLE, + status: FieldStatusEnum.INACTIVE, + }); + const fields = [field]; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + + await expect(feedbackService.updateFeedback(dto)).rejects.toThrow( + new BadRequestException('this field is disabled'), + ); + }); + }); + + describe('addIssue', () => { + it('should add issue to feedback successfully', async () => { + const dto = new AddIssueDto(); + dto.feedbackId = faker.number.int(); + dto.issueId = faker.number.int(); + dto.channelId = faker.number.int(); + + jest.spyOn(feedbackMySQLService, 'addIssue').mockResolvedValue(undefined); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + jest.spyOn(eventEmitter, 'emit').mockImplementation(() => true); + + await feedbackService.addIssue(dto); + + expect(feedbackMySQLService.addIssue).toHaveBeenCalledWith(dto); + expect(eventEmitter.emit).toHaveBeenCalled(); + }); + it('should add issue with OpenSearch enabled', async () => { + const dto = new AddIssueDto(); + dto.feedbackId = faker.number.int(); + dto.issueId = faker.number.int(); + dto.channelId = faker.number.int(); + + jest.spyOn(feedbackMySQLService, 'addIssue').mockResolvedValue(undefined); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return true; + return false; + }); + jest + .spyOn(feedbackOSService, 'upsertFeedbackItem') + .mockResolvedValue(undefined); + jest.spyOn(eventEmitter, 'emit').mockImplementation(() => true); + + await feedbackService.addIssue(dto); + + expect(feedbackMySQLService.addIssue).toHaveBeenCalledWith(dto); + expect(eventEmitter.emit).toHaveBeenCalled(); + }); + }); + + describe('removeIssue', () => { + it('should remove issue from feedback successfully', async () => { + const dto = new RemoveIssueDto(); + dto.feedbackId = faker.number.int(); + dto.issueId = faker.number.int(); + dto.channelId = faker.number.int(); + + jest + .spyOn(feedbackMySQLService, 'removeIssue') + .mockResolvedValue(undefined); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + + await feedbackService.removeIssue(dto); + + expect(feedbackMySQLService.removeIssue).toHaveBeenCalledWith(dto); + }); + it('should remove issue with OpenSearch enabled', async () => { + const dto = new RemoveIssueDto(); + dto.feedbackId = faker.number.int(); + dto.issueId = faker.number.int(); + dto.channelId = faker.number.int(); + + jest + .spyOn(feedbackMySQLService, 'removeIssue') + .mockResolvedValue(undefined); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return true; + return false; + }); + jest + .spyOn(feedbackOSService, 'upsertFeedbackItem') + .mockResolvedValue(undefined); + + await feedbackService.removeIssue(dto); + + expect(feedbackMySQLService.removeIssue).toHaveBeenCalledWith(dto); + }); + }); + + describe('countByProjectId', () => { + it('should count feedbacks by project id', async () => { + const dto = new CountByProjectIdDto(); + dto.projectId = faker.number.int(); + const expectedCount = faker.number.int(); + + jest + .spyOn(feedbackMySQLService, 'countByProjectId') + .mockResolvedValue(expectedCount); + + const result = await feedbackService.countByProjectId(dto); + + expect(result).toEqual({ total: expectedCount }); + expect(feedbackMySQLService.countByProjectId).toHaveBeenCalledWith(dto); + }); + }); + + describe('deleteByIds', () => { + it('should delete feedbacks by ids successfully', async () => { + const dto = new DeleteByIdsDto(); + dto.channelId = faker.number.int(); + dto.feedbackIds = [faker.number.int(), faker.number.int()]; + + jest + .spyOn(feedbackMySQLService, 'deleteByIds') + .mockResolvedValue(undefined); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + + await feedbackService.deleteByIds(dto); + + expect(feedbackMySQLService.deleteByIds).toHaveBeenCalledWith(dto); + }); + it('should delete feedbacks with OpenSearch enabled', async () => { + const dto = new DeleteByIdsDto(); + dto.channelId = faker.number.int(); + dto.feedbackIds = [faker.number.int(), faker.number.int()]; + + jest + .spyOn(feedbackMySQLService, 'deleteByIds') + .mockResolvedValue(undefined); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return true; + return false; + }); + jest.spyOn(feedbackOSService, 'deleteByIds').mockResolvedValue(undefined); + + await feedbackService.deleteByIds(dto); + + expect(feedbackMySQLService.deleteByIds).toHaveBeenCalledWith(dto); + }); + }); + + describe('findById', () => { + it('should find feedback by id with MySQL', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const mockFeedback = { + id: feedbackId, + data: {}, + createdAt: new Date(), + updatedAt: new Date(), + issues: [], + }; + + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + jest + .spyOn(feedbackMySQLService, 'findById') + .mockResolvedValue(mockFeedback); + + const result = await feedbackService.findById({ channelId, feedbackId }); + + expect(result).toEqual(mockFeedback); + expect(feedbackMySQLService.findById).toHaveBeenCalledWith({ + feedbackId, + }); + }); + it('should find feedback by id with OpenSearch', async () => { + const channelId = faker.number.int(); + const feedbackId = faker.number.int(); + const mockFeedback = { + id: feedbackId, + data: {}, + createdAt: new Date(), + updatedAt: new Date(), + issues: [], + }; + + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return true; + return false; + }); + jest.spyOn(feedbackOSService, 'findById').mockResolvedValue({ + items: [mockFeedback], + total: 1, + }); + jest.spyOn(issueService, 'findIssuesByFeedbackIds').mockResolvedValue({}); + + const result = await feedbackService.findById({ channelId, feedbackId }); + + expect(result.id).toBe(feedbackId); + }); + }); + + describe('generateFile', () => { + it('should generate XLSX file successfully', async () => { + const dto = new GenerateExcelDto(); + dto.projectId = faker.number.int(); + dto.channelId = faker.number.int(); + dto.type = 'xlsx'; + dto.queries = []; + dto.defaultQueries = []; + dto.operator = 'AND'; + dto.sort = {}; + dto.fieldIds = [faker.number.int()]; + + const fields = [createFieldDto()]; + const mockProject = { timezone: { name: 'UTC' } } as unknown as any; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue(fields as FieldEntity[]); + jest + .spyOn(fieldService, 'findByIds') + .mockResolvedValue(fields as FieldEntity[]); + jest.spyOn(channelService, 'findById').mockResolvedValue({ + feedbackSearchMaxDays: 30, + project: { id: faker.number.int() }, + } as unknown as any); + jest.spyOn(projectService, 'findById').mockResolvedValue(mockProject); + jest.spyOn(configService, 'get').mockImplementation((key: string) => { + if (key === 'opensearch.use') return false; + return false; + }); + jest.spyOn(feedbackMySQLService, 'findByChannelIdV2').mockResolvedValue({ + items: [], + meta: { + totalItems: 0, + itemCount: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 1, + }, + }); + jest.spyOn(issueService, 'findIssuesByFeedbackIds').mockResolvedValue({}); + + const result = await feedbackService.generateFile(dto); + + expect(result.streamableFile).toBeDefined(); + expect(result.feedbackIds).toBeDefined(); + }); + it('should throw error for invalid channel in generateFile', async () => { + const dto = new GenerateExcelDto(); + dto.projectId = faker.number.int(); + dto.channelId = faker.number.int(); + dto.type = 'xlsx'; + + jest + .spyOn(fieldService, 'findByChannelId') + .mockResolvedValue([] as FieldEntity[]); + + await expect(feedbackService.generateFile(dto)).rejects.toThrow( + new BadRequestException('invalid channel'), + ); + }); }); }); diff --git a/apps/api/src/domains/admin/project/ai/ai.controller.spec.ts b/apps/api/src/domains/admin/project/ai/ai.controller.spec.ts new file mode 100644 index 000000000..c5968784a --- /dev/null +++ b/apps/api/src/domains/admin/project/ai/ai.controller.spec.ts @@ -0,0 +1,770 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; + +import { AIProvidersEnum } from '@/common/enums/ai-providers.enum'; +import { getMockProvider, MockDataSource } from '@/test-utils/util-functions'; +import { AIController } from './ai.controller'; +import { AIService } from './ai.service'; +import type { + CreateAIFieldTemplateRequestDto, + CreateAIIssueTemplateRequestDto, + GetAIIssuePlaygroundResultRequestDto, + GetAIPlaygroundResultRequestDto, + ProcessAIFieldRequestDto, + ProcessSingleAIFieldRequestDto, + UpdateAIIntegrationsRequestDto, + ValidteAPIKeyRequestDto, +} from './dtos/requests'; +import type { ValidateAPIKeyResponseDto } from './dtos/responses'; + +const MockAIService = { + validateAPIKey: jest.fn(), + getIntegration: jest.fn(), + upsertIntegration: jest.fn(), + getModels: jest.fn(), + findFieldTemplatesByProjectId: jest.fn(), + createNewFieldTemplate: jest.fn(), + updateFieldTemplate: jest.fn(), + deleteFieldTemplateById: jest.fn(), + findIssueTemplatesByProjectId: jest.fn(), + createNewIssueTemplate: jest.fn(), + updateIssueTemplate: jest.fn(), + deleteIssueTemplateById: jest.fn(), + processFeedbacksAIFields: jest.fn(), + processAIField: jest.fn(), + getPlaygroundPromptResult: jest.fn(), + recommendAIIssue: jest.fn(), + getIssuePlaygroundPromptResult: jest.fn(), + getUsages: jest.fn(), +}; + +describe('AIController', () => { + let aiController: AIController; + + beforeEach(async () => { + // Reset all mocks before each test + jest.clearAllMocks(); + + const module = await Test.createTestingModule({ + controllers: [AIController], + providers: [ + getMockProvider(AIService, MockAIService), + getMockProvider(DataSource, MockDataSource), + ], + }).compile(); + + aiController = module.get(AIController); + }); + + describe('validateAPIKey', () => { + it('should validate API key successfully', async () => { + const body: ValidteAPIKeyRequestDto = { + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + }; + const mockResult: ValidateAPIKeyResponseDto = { valid: true }; + + jest.spyOn(MockAIService, 'validateAPIKey').mockResolvedValue(mockResult); + + const result = await aiController.validateAPIKey(body); + + expect(MockAIService.validateAPIKey).toHaveBeenCalledWith( + body.provider, + body.apiKey, + body.endpointUrl, + ); + expect(result).toEqual(mockResult); + }); + + it('should return invalid API key result', async () => { + const body: ValidteAPIKeyRequestDto = { + provider: AIProvidersEnum.OPEN_AI, + apiKey: 'invalid-key', + endpointUrl: faker.internet.url(), + }; + const mockResult: ValidateAPIKeyResponseDto = { + valid: false, + error: 'Invalid API key', + }; + + jest.spyOn(MockAIService, 'validateAPIKey').mockResolvedValue(mockResult); + + const result = await aiController.validateAPIKey(body); + + expect(MockAIService.validateAPIKey).toHaveBeenCalledWith( + body.provider, + body.apiKey, + body.endpointUrl, + ); + expect(result).toEqual(mockResult); + }); + }); + + describe('getIntegration', () => { + it('should get AI integration successfully', async () => { + const projectId = faker.number.int(); + const mockIntegration = { + id: faker.number.int(), + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + systemPrompt: faker.string.sample(), + tokenThreshold: faker.number.int(), + }; + + jest + .spyOn(MockAIService, 'getIntegration') + .mockResolvedValue(mockIntegration); + + const result = await aiController.getIntegration(projectId); + + expect(MockAIService.getIntegration).toHaveBeenCalledWith(projectId); + expect(result).toBeDefined(); + }); + + it('should return null when no integration found', async () => { + const projectId = faker.number.int(); + + jest.spyOn(MockAIService, 'getIntegration').mockResolvedValue(null); + + const result = await aiController.getIntegration(projectId); + + expect(MockAIService.getIntegration).toHaveBeenCalledWith(projectId); + expect(result).toBeNull(); + }); + }); + + describe('updateIntegration', () => { + it('should update AI integration successfully', async () => { + const projectId = faker.number.int(); + const body: UpdateAIIntegrationsRequestDto = { + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + systemPrompt: faker.string.sample(), + tokenThreshold: faker.number.int(), + }; + const mockIntegration = { + id: faker.number.int(), + ...body, + projectId, + }; + + jest + .spyOn(MockAIService, 'upsertIntegration') + .mockResolvedValue(mockIntegration); + + const result = await aiController.updateIntegration(projectId, body); + + expect(MockAIService.upsertIntegration).toHaveBeenCalled(); + expect(result).toBeDefined(); + }); + }); + + describe('getModels', () => { + it('should get AI models successfully', async () => { + const projectId = faker.number.int(); + const mockModels = [ + { id: 'gpt-4o', name: 'GPT-4o' }, + { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' }, + ]; + + jest.spyOn(MockAIService, 'getModels').mockResolvedValue(mockModels); + + const result = await aiController.getModels(projectId); + + expect(MockAIService.getModels).toHaveBeenCalledWith(projectId); + expect(result).toBeDefined(); + }); + + it('should return empty array when no models found', async () => { + const projectId = faker.number.int(); + + jest.spyOn(MockAIService, 'getModels').mockResolvedValue([]); + + const result = await aiController.getModels(projectId); + + expect(MockAIService.getModels).toHaveBeenCalledWith(projectId); + expect(result).toEqual({ models: [] }); + }); + }); + + describe('getFieldTemplates', () => { + it('should get AI field templates successfully', async () => { + const projectId = faker.number.int(); + const mockTemplates = [ + { + id: faker.number.int(), + title: 'Summary', + prompt: 'Summarize the feedback', + model: 'gpt-4o', + temperature: 0.5, + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + }, + ]; + + jest + .spyOn(MockAIService, 'findFieldTemplatesByProjectId') + .mockResolvedValue(mockTemplates); + + const result = await aiController.getFieldTemplates(projectId); + + expect(MockAIService.findFieldTemplatesByProjectId).toHaveBeenCalledWith( + projectId, + ); + expect(result).toBeDefined(); + }); + }); + + describe('createNewFieldTemplate', () => { + it('should create new AI field template successfully', async () => { + const projectId = faker.number.int(); + const body: CreateAIFieldTemplateRequestDto = { + title: faker.string.sample(), + prompt: faker.string.sample(), + model: 'gpt-4o', + temperature: 0.5, + }; + const mockTemplate = { + id: faker.number.int(), + ...body, + projectId, + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + }; + + jest + .spyOn(MockAIService, 'createNewFieldTemplate') + .mockResolvedValue(mockTemplate); + + const result = await aiController.createNewFieldTemplate(projectId, body); + + expect(MockAIService.createNewFieldTemplate).toHaveBeenCalledWith({ + ...body, + projectId, + }); + expect(result).toBeDefined(); + }); + }); + + describe('updateFieldTemplate', () => { + it('should update AI field template successfully', async () => { + const projectId = faker.number.int(); + const templateId = faker.number.int(); + const body: CreateAIFieldTemplateRequestDto = { + title: faker.string.sample(), + prompt: faker.string.sample(), + model: 'gpt-4o', + temperature: 0.5, + }; + + jest + .spyOn(MockAIService, 'updateFieldTemplate') + .mockResolvedValue(undefined); + + await aiController.updateFieldTemplate(projectId, templateId, body); + + expect(MockAIService.updateFieldTemplate).toHaveBeenCalledWith({ + ...body, + projectId, + templateId, + }); + }); + }); + + describe('deleteFieldTemplate', () => { + it('should delete AI field template successfully', async () => { + const projectId = faker.number.int(); + const templateId = faker.number.int(); + + jest + .spyOn(MockAIService, 'deleteFieldTemplateById') + .mockResolvedValue(undefined); + + await aiController.deleteFieldTemplate(projectId, templateId); + + expect(MockAIService.deleteFieldTemplateById).toHaveBeenCalledWith( + projectId, + templateId, + ); + }); + }); + + describe('getIssueTemplates', () => { + it('should get AI issue templates successfully', async () => { + const projectId = faker.number.int(); + const mockTemplates = [ + { + id: faker.number.int(), + channelId: faker.number.int(), + targetFieldKeys: ['message', 'title'], + prompt: 'Generate issue recommendations', + isEnabled: true, + model: 'gpt-4o', + temperature: 0.5, + dataReferenceAmount: 3, + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + }, + ]; + + jest + .spyOn(MockAIService, 'findIssueTemplatesByProjectId') + .mockResolvedValue(mockTemplates); + + const result = await aiController.getIssueTemplates(projectId); + + expect(MockAIService.findIssueTemplatesByProjectId).toHaveBeenCalledWith( + projectId, + ); + expect(result).toBeDefined(); + }); + }); + + describe('createNewIssueTemplate', () => { + it('should create new AI issue template successfully', async () => { + const projectId = faker.number.int(); + const body: CreateAIIssueTemplateRequestDto = { + channelId: faker.number.int(), + targetFieldKeys: ['message', 'title'], + prompt: 'Generate issue recommendations', + isEnabled: true, + model: 'gpt-4o', + temperature: 0.5, + dataReferenceAmount: 3, + }; + const mockTemplate = { + id: faker.number.int(), + ...body, + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + }; + + jest + .spyOn(MockAIService, 'createNewIssueTemplate') + .mockResolvedValue(mockTemplate); + + const result = await aiController.createNewIssueTemplate(projectId, body); + + expect(MockAIService.createNewIssueTemplate).toHaveBeenCalledWith({ + ...body, + }); + expect(result).toBeDefined(); + }); + }); + + describe('updateIssueTemplate', () => { + it('should update AI issue template successfully', async () => { + const templateId = faker.number.int(); + const body: CreateAIIssueTemplateRequestDto = { + channelId: faker.number.int(), + targetFieldKeys: ['message', 'title'], + prompt: 'Generate issue recommendations', + isEnabled: true, + model: 'gpt-4o', + temperature: 0.5, + dataReferenceAmount: 3, + }; + + jest + .spyOn(MockAIService, 'updateIssueTemplate') + .mockResolvedValue(undefined); + + await aiController.updateIssueTemplate(templateId, body); + + expect(MockAIService.updateIssueTemplate).toHaveBeenCalledWith({ + ...body, + templateId, + }); + }); + }); + + describe('deleteIssueTemplate', () => { + it('should delete AI issue template successfully', async () => { + const templateId = faker.number.int(); + + jest + .spyOn(MockAIService, 'deleteIssueTemplateById') + .mockResolvedValue(undefined); + + await aiController.deleteIssueTemplate(templateId); + + expect(MockAIService.deleteIssueTemplateById).toHaveBeenCalledWith( + templateId, + ); + }); + }); + + describe('processAIFields', () => { + it('should process AI fields successfully', async () => { + const body: ProcessAIFieldRequestDto = { + feedbackIds: [faker.number.int(), faker.number.int()], + }; + + jest + .spyOn(MockAIService, 'processFeedbacksAIFields') + .mockResolvedValue(undefined); + + await aiController.processAIFields(body); + + expect(MockAIService.processFeedbacksAIFields).toHaveBeenCalledWith( + body.feedbackIds, + ); + }); + }); + + describe('processAIField', () => { + it('should process single AI field successfully', async () => { + const body: ProcessSingleAIFieldRequestDto = { + feedbackId: faker.number.int(), + aiFieldId: faker.number.int(), + }; + + jest.spyOn(MockAIService, 'processAIField').mockResolvedValue(undefined); + + await aiController.processAIField(body); + + expect(MockAIService.processAIField).toHaveBeenCalledWith( + body.feedbackId, + body.aiFieldId, + ); + }); + }); + + describe('getPlaygroundResult', () => { + it('should get playground result successfully', async () => { + const projectId = faker.number.int(); + const body: GetAIPlaygroundResultRequestDto = { + model: 'gpt-4o', + temperature: 0.5, + templatePrompt: 'Test prompt', + temporaryFields: [ + { + name: 'message', + description: 'User message', + value: 'Test message', + }, + ], + }; + const mockResult = 'Test result'; + + jest + .spyOn(MockAIService, 'getPlaygroundPromptResult') + .mockResolvedValue(mockResult); + + const result = await aiController.getPlaygroundResult(projectId, body); + + expect(MockAIService.getPlaygroundPromptResult).toHaveBeenCalledWith({ + ...body, + projectId, + }); + expect(result).toBeDefined(); + }); + }); + + describe('recommendAIIssue', () => { + it('should recommend AI issue successfully', async () => { + const feedbackId = faker.number.int(); + const mockResult = { + success: true, + result: [{ issueName: 'Bug Report' }, { issueName: 'Feature Request' }], + }; + + jest + .spyOn(MockAIService, 'recommendAIIssue') + .mockResolvedValue(mockResult); + + const result = await aiController.recommendAIIssue(feedbackId); + + expect(MockAIService.recommendAIIssue).toHaveBeenCalledWith(feedbackId); + expect(result).toEqual(mockResult); + }); + }); + + describe('getAIIssuePlaygroundResult', () => { + it('should get AI issue playground result successfully', async () => { + const projectId = faker.number.int(); + const body: GetAIIssuePlaygroundResultRequestDto = { + channelId: faker.number.int(), + targetFieldKeys: ['message', 'title'], + model: 'gpt-4o', + temperature: 0.5, + templatePrompt: 'Test prompt', + dataReferenceAmount: 3, + temporaryFields: [ + { + name: 'message', + description: 'User message', + value: 'Test message', + }, + ], + }; + const mockResult = ['Issue 1', 'Issue 2']; + + jest + .spyOn(MockAIService, 'getIssuePlaygroundPromptResult') + .mockResolvedValue(mockResult); + + const result = await aiController.getAIIssuePlaygroundResult( + projectId, + body, + ); + + expect(MockAIService.getIssuePlaygroundPromptResult).toHaveBeenCalledWith( + { + ...body, + }, + ); + expect(result).toBeDefined(); + }); + }); + + describe('getUsages', () => { + it('should get AI usages successfully', async () => { + const projectId = faker.number.int(); + const from = faker.date.past(); + const to = faker.date.recent(); + const mockUsages = [ + { + year: 2024, + month: 1, + day: 15, + category: 'AI_FIELD', + provider: AIProvidersEnum.OPEN_AI, + usedTokens: 1000, + }, + ]; + + jest.spyOn(MockAIService, 'getUsages').mockResolvedValue(mockUsages); + + const result = await aiController.getUsages(projectId, from, to); + + expect(MockAIService.getUsages).toHaveBeenCalledWith(projectId, from, to); + expect(result).toBeDefined(); + }); + }); + + describe('Error Cases', () => { + it('should handle service errors in validateAPIKey', async () => { + const body: ValidteAPIKeyRequestDto = { + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + }; + + jest + .spyOn(MockAIService, 'validateAPIKey') + .mockRejectedValue(new BadRequestException('Invalid provider')); + + await expect(aiController.validateAPIKey(body)).rejects.toThrow( + 'Invalid provider', + ); + }); + + it('should handle service errors in getIntegration', async () => { + const projectId = faker.number.int(); + + jest + .spyOn(MockAIService, 'getIntegration') + .mockRejectedValue(new NotFoundException('Project not found')); + + await expect(aiController.getIntegration(projectId)).rejects.toThrow( + 'Project not found', + ); + }); + + it('should handle service errors in updateIntegration', async () => { + const projectId = faker.number.int(); + const body: UpdateAIIntegrationsRequestDto = { + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + systemPrompt: faker.string.sample(), + tokenThreshold: faker.number.int(), + }; + + jest + .spyOn(MockAIService, 'upsertIntegration') + .mockRejectedValue(new BadRequestException('Invalid API key')); + + await expect( + aiController.updateIntegration(projectId, body), + ).rejects.toThrow('Invalid API key'); + }); + + it('should handle service errors in getModels', async () => { + const projectId = faker.number.int(); + + jest + .spyOn(MockAIService, 'getModels') + .mockRejectedValue(new NotFoundException('Integration not found')); + + await expect(aiController.getModels(projectId)).rejects.toThrow( + 'Integration not found', + ); + }); + + it('should handle service errors in createNewFieldTemplate', async () => { + const projectId = faker.number.int(); + const body: CreateAIFieldTemplateRequestDto = { + title: faker.string.sample(), + prompt: faker.string.sample(), + model: 'gpt-4o', + temperature: 0.5, + }; + + jest + .spyOn(MockAIService, 'createNewFieldTemplate') + .mockRejectedValue(new BadRequestException('Template already exists')); + + await expect( + aiController.createNewFieldTemplate(projectId, body), + ).rejects.toThrow('Template already exists'); + }); + + it('should handle service errors in updateFieldTemplate', async () => { + const projectId = faker.number.int(); + const templateId = faker.number.int(); + const body: CreateAIFieldTemplateRequestDto = { + title: faker.string.sample(), + prompt: faker.string.sample(), + model: 'gpt-4o', + temperature: 0.5, + }; + + jest + .spyOn(MockAIService, 'updateFieldTemplate') + .mockRejectedValue(new NotFoundException('Template not found')); + + await expect( + aiController.updateFieldTemplate(projectId, templateId, body), + ).rejects.toThrow('Template not found'); + }); + + it('should handle service errors in deleteFieldTemplate', async () => { + const projectId = faker.number.int(); + const templateId = faker.number.int(); + + jest + .spyOn(MockAIService, 'deleteFieldTemplateById') + .mockRejectedValue(new NotFoundException('Template not found')); + + await expect( + aiController.deleteFieldTemplate(projectId, templateId), + ).rejects.toThrow('Template not found'); + }); + + it('should handle service errors in processAIFields', async () => { + const body: ProcessAIFieldRequestDto = { + feedbackIds: [faker.number.int()], + }; + + jest + .spyOn(MockAIService, 'processFeedbacksAIFields') + .mockRejectedValue(new BadRequestException('Token threshold exceeded')); + + await expect(aiController.processAIFields(body)).rejects.toThrow( + 'Token threshold exceeded', + ); + }); + + it('should handle service errors in processAIField', async () => { + const body: ProcessSingleAIFieldRequestDto = { + feedbackId: faker.number.int(), + aiFieldId: faker.number.int(), + }; + + jest + .spyOn(MockAIService, 'processAIField') + .mockRejectedValue(new NotFoundException('Feedback not found')); + + await expect(aiController.processAIField(body)).rejects.toThrow( + 'Feedback not found', + ); + }); + + it('should handle service errors in getPlaygroundResult', async () => { + const projectId = faker.number.int(); + const body: GetAIPlaygroundResultRequestDto = { + model: 'gpt-4o', + temperature: 0.5, + templatePrompt: 'Test prompt', + temporaryFields: [], + }; + + jest + .spyOn(MockAIService, 'getPlaygroundPromptResult') + .mockRejectedValue(new BadRequestException('Token threshold exceeded')); + + await expect( + aiController.getPlaygroundResult(projectId, body), + ).rejects.toThrow('Token threshold exceeded'); + }); + + it('should handle service errors in recommendAIIssue', async () => { + const feedbackId = faker.number.int(); + + jest + .spyOn(MockAIService, 'recommendAIIssue') + .mockRejectedValue(new NotFoundException('Feedback not found')); + + await expect(aiController.recommendAIIssue(feedbackId)).rejects.toThrow( + 'Feedback not found', + ); + }); + + it('should handle service errors in getAIIssuePlaygroundResult', async () => { + const projectId = faker.number.int(); + const body: GetAIIssuePlaygroundResultRequestDto = { + channelId: faker.number.int(), + targetFieldKeys: ['message', 'title'], + model: 'gpt-4o', + temperature: 0.5, + templatePrompt: 'Test prompt', + dataReferenceAmount: 3, + temporaryFields: [], + }; + + jest + .spyOn(MockAIService, 'getIssuePlaygroundPromptResult') + .mockRejectedValue(new NotFoundException('Channel not found')); + + await expect( + aiController.getAIIssuePlaygroundResult(projectId, body), + ).rejects.toThrow('Channel not found'); + }); + + it('should handle service errors in getUsages', async () => { + const projectId = faker.number.int(); + const from = faker.date.past(); + const to = faker.date.recent(); + + jest + .spyOn(MockAIService, 'getUsages') + .mockRejectedValue(new NotFoundException('Project not found')); + + await expect(aiController.getUsages(projectId, from, to)).rejects.toThrow( + 'Project not found', + ); + }); + }); +}); diff --git a/apps/api/src/domains/admin/project/ai/ai.service.spec.ts b/apps/api/src/domains/admin/project/ai/ai.service.spec.ts new file mode 100644 index 000000000..6cac89292 --- /dev/null +++ b/apps/api/src/domains/admin/project/ai/ai.service.spec.ts @@ -0,0 +1,1255 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import type { Repository } from 'typeorm'; + +import { FieldFormatEnum, FieldStatusEnum } from '@/common/enums'; +import { AIPromptStatusEnum } from '@/common/enums/ai-prompt-status.enum'; +import { AIProvidersEnum } from '@/common/enums/ai-providers.enum'; +import { ChannelEntity } from '@/domains/admin/channel/channel/channel.entity'; +import { FieldEntity } from '@/domains/admin/channel/field/field.entity'; +import { FeedbackEntity } from '@/domains/admin/feedback/feedback.entity'; +import { FeedbackMySQLService } from '@/domains/admin/feedback/feedback.mysql.service'; +import { FeedbackOSService } from '@/domains/admin/feedback/feedback.os.service'; +import { IssueEntity } from '@/domains/admin/project/issue/issue.entity'; +import { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { RoleEntity } from '@/domains/admin/project/role/role.entity'; +import { mockRepository, TestConfig } from '@/test-utils/util-functions'; +import { AIFieldTemplatesEntity } from './ai-field-templates.entity'; +import { AIIntegrationsEntity } from './ai-integrations.entity'; +import { AIIssueTemplatesEntity } from './ai-issue-templates.entity'; +import { AIUsagesEntity, UsageCategoryEnum } from './ai-usages.entity'; +import { AIService } from './ai.service'; +import { CreateAIFieldTemplateDto } from './dtos/create-ai-field-template.dto'; +import { CreateAIIntegrationsDto } from './dtos/create-ai-integrations.dto'; +import { CreateAIIssueTemplateDto } from './dtos/create-ai-issue-template.dto'; +import { GetAIIssuePlaygroundResultDto } from './dtos/get-ai-issue-playground-result.dto'; +import { GetAIPlaygroundResultDto } from './dtos/get-ai-playground-result.dto'; +import { UpdateAIFieldTemplateDto } from './dtos/update-ai-field-template.dto'; +import { UpdateAIIssueTemplateDto } from './dtos/update-ai-issue-template.dto'; + +// Mock AIClient +jest.mock('./ai.client', () => ({ + AIClient: jest.fn().mockImplementation(() => ({ + validateAPIKey: jest.fn(), + getModelList: jest.fn(), + executePrompt: jest.fn(), + executeIssueRecommend: jest.fn(), + })), + PromptParameters: jest.fn(), + IssueRecommendParameters: jest.fn(), +})); + +describe('AIService', () => { + let aiService: AIService; + let aiIntegrationsRepo: Repository; + let aiFieldTemplatesRepo: Repository; + let aiIssueTemplatesRepo: Repository; + let aiUsagesRepo: Repository; + let feedbackRepo: Repository; + let issueRepo: Repository; + let fieldRepo: Repository; + let channelRepo: Repository; + let projectRepo: Repository; + let roleRepo: Repository; + let _feedbackMySQLService: FeedbackMySQLService; + let _feedbackOSService: FeedbackOSService; + let _configService: ConfigService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [TestConfig], + providers: [ + AIService, + { + provide: getRepositoryToken(AIIntegrationsEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(AIFieldTemplatesEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(AIIssueTemplatesEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(AIUsagesEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(FeedbackEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(IssueEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(FieldEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(ChannelEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(ProjectEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(RoleEntity), + useFactory: mockRepository, + }, + { + provide: FeedbackMySQLService, + useValue: { + updateFeedback: jest.fn(), + }, + }, + { + provide: FeedbackOSService, + useValue: { + upsertFeedbackItem: jest.fn(), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue(false), + }, + }, + ], + }).compile(); + + aiService = module.get(AIService); + aiIntegrationsRepo = module.get(getRepositoryToken(AIIntegrationsEntity)); + aiFieldTemplatesRepo = module.get( + getRepositoryToken(AIFieldTemplatesEntity), + ); + aiIssueTemplatesRepo = module.get( + getRepositoryToken(AIIssueTemplatesEntity), + ); + aiUsagesRepo = module.get(getRepositoryToken(AIUsagesEntity)); + feedbackRepo = module.get(getRepositoryToken(FeedbackEntity)); + issueRepo = module.get(getRepositoryToken(IssueEntity)); + fieldRepo = module.get(getRepositoryToken(FieldEntity)); + channelRepo = module.get(getRepositoryToken(ChannelEntity)); + projectRepo = module.get(getRepositoryToken(ProjectEntity)); + roleRepo = module.get(getRepositoryToken(RoleEntity)); + _feedbackMySQLService = + module.get(FeedbackMySQLService); + _feedbackOSService = module.get(FeedbackOSService); + _configService = module.get(ConfigService); + }); + + describe('validateAPIKey', () => { + it('should successfully validate a valid API key', async () => { + const provider = AIProvidersEnum.OPEN_AI; + const apiKey = faker.string.alphanumeric(32); + const endpointUrl = faker.internet.url(); + + const mockClient = { + validateAPIKey: jest.fn().mockResolvedValue(undefined), + }; + jest.spyOn(await import('./ai.client'), 'AIClient').mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + () => mockClient as unknown as jest.MockedClass, + ); + + const result = await aiService.validateAPIKey( + provider, + apiKey, + endpointUrl, + ); + + expect(result.valid).toBe(true); + expect(mockClient.validateAPIKey).toHaveBeenCalled(); + }); + + it('should fail to validate an invalid API key', async () => { + const provider = AIProvidersEnum.OPEN_AI; + const apiKey = 'invalid-key'; + const endpointUrl = faker.internet.url(); + const errorMessage = 'Invalid API key'; + + const mockClient = { + validateAPIKey: jest.fn().mockRejectedValue(new Error(errorMessage)), + }; + jest.spyOn(await import('./ai.client'), 'AIClient').mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + () => mockClient as unknown as jest.MockedClass, + ); + + const result = await aiService.validateAPIKey( + provider, + apiKey, + endpointUrl, + ); + + expect(result.valid).toBe(false); + expect(result.error).toBe(errorMessage); + }); + }); + + describe('getIntegration', () => { + it('should retrieve integration information by project ID', async () => { + const projectId = faker.number.int(); + const mockIntegration = { + id: faker.number.int(), + projectId, + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + }; + + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue(mockIntegration as unknown as AIIntegrationsEntity); + + const result = await aiService.getIntegration(projectId); + + expect(result).toEqual(mockIntegration); + expect(aiIntegrationsRepo.findOne).toHaveBeenCalledWith({ + where: { project: { id: projectId } }, + }); + }); + + it('should return null when integration information does not exist', async () => { + const projectId = faker.number.int(); + + jest.spyOn(aiIntegrationsRepo, 'findOne').mockResolvedValue(null); + + const result = await aiService.getIntegration(projectId); + + expect(result).toBeNull(); + }); + }); + + describe('upsertIntegration', () => { + it('should create new integration information', async () => { + const dto = new CreateAIIntegrationsDto(); + dto.projectId = faker.number.int(); + dto.provider = AIProvidersEnum.OPEN_AI; + dto.apiKey = faker.string.alphanumeric(32); + dto.endpointUrl = faker.internet.url(); + dto.systemPrompt = faker.lorem.sentence(); + dto.tokenThreshold = faker.number.int({ min: 1000, max: 10000 }); + dto.notificationThreshold = faker.number.int({ min: 100, max: 1000 }); + + jest.spyOn(aiIntegrationsRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(aiIntegrationsRepo, 'save').mockResolvedValue(dto as any); + jest + .spyOn(aiService, 'createDefaultFieldTemplates') + .mockResolvedValue(undefined); + + await aiService.upsertIntegration(dto); + + expect(aiIntegrationsRepo.save).toHaveBeenCalled(); + expect(aiService.createDefaultFieldTemplates).toHaveBeenCalledWith( + dto.projectId, + ); + }); + + it('should update existing integration information', async () => { + const dto = new CreateAIIntegrationsDto(); + dto.projectId = faker.number.int(); + dto.provider = AIProvidersEnum.OPEN_AI; + dto.apiKey = faker.string.alphanumeric(32); + + const existingIntegration = { + id: faker.number.int(), + projectId: dto.projectId, + provider: AIProvidersEnum.GEMINI, + apiKey: 'old-key', + }; + + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue( + existingIntegration as unknown as AIIntegrationsEntity, + ); + jest + .spyOn(aiIntegrationsRepo, 'save') + .mockResolvedValue({ ...existingIntegration, ...dto } as any); + + await aiService.upsertIntegration(dto); + + expect(aiIntegrationsRepo.save).toHaveBeenCalledWith({ + ...existingIntegration, + ...dto, + }); + }); + }); + + describe('findFieldTemplatesByProjectId', () => { + it('should retrieve field template list by project ID', async () => { + const projectId = faker.number.int(); + const mockTemplates = [ + { + id: faker.number.int(), + title: 'Summary', + prompt: 'Summarize the feedback', + model: 'gpt-4o', + temperature: 0.5, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: faker.number.int(), + title: 'Sentiment Analysis', + prompt: 'Analyze sentiment', + model: 'gpt-4o', + temperature: 0.5, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + jest + .spyOn(aiFieldTemplatesRepo, 'find') + .mockResolvedValue(mockTemplates as any); + + const result = await aiService.findFieldTemplatesByProjectId(projectId); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: mockTemplates[0].id, + title: mockTemplates[0].title, + prompt: mockTemplates[0].prompt, + model: mockTemplates[0].model, + temperature: mockTemplates[0].temperature, + createdAt: mockTemplates[0].createdAt, + updatedAt: mockTemplates[0].updatedAt, + }); + }); + }); + + describe('createNewFieldTemplate', () => { + it('should create a new field template', async () => { + const dto = new CreateAIFieldTemplateDto(); + dto.projectId = faker.number.int(); + dto.title = faker.lorem.words(2); + dto.prompt = faker.lorem.sentence(); + dto.model = 'gpt-4o'; + dto.temperature = 0.5; + + const mockTemplate = { + id: faker.number.int(), + ...dto, + createdAt: new Date(), + updatedAt: new Date(), + }; + + jest + .spyOn(aiFieldTemplatesRepo, 'save') + .mockResolvedValue(mockTemplate as any); + + await aiService.createNewFieldTemplate(dto); + + expect(aiFieldTemplatesRepo.save).toHaveBeenCalled(); + }); + }); + + describe('updateFieldTemplate', () => { + it('should update existing field template', async () => { + const dto = new UpdateAIFieldTemplateDto(); + dto.templateId = faker.number.int(); + dto.projectId = faker.number.int(); + dto.title = faker.lorem.words(2); + dto.prompt = faker.lorem.sentence(); + + const existingTemplate = { + id: dto.templateId, + projectId: dto.projectId, + title: 'Old Title', + prompt: 'Old Prompt', + model: 'gpt-4o', + temperature: 0.5, + }; + + jest + .spyOn(aiFieldTemplatesRepo, 'findOne') + .mockResolvedValue(existingTemplate as any); + jest + .spyOn(aiFieldTemplatesRepo, 'save') + .mockResolvedValue({ ...existingTemplate, ...dto } as any); + + await aiService.updateFieldTemplate(dto); + + expect(aiFieldTemplatesRepo.save).toHaveBeenCalledWith({ + ...existingTemplate, + ...dto, + }); + }); + + it('should throw exception when updating non-existent template', async () => { + const dto = new UpdateAIFieldTemplateDto(); + dto.templateId = faker.number.int(); + dto.projectId = faker.number.int(); + + jest.spyOn(aiFieldTemplatesRepo, 'findOne').mockResolvedValue(null); + + await expect(aiService.updateFieldTemplate(dto)).rejects.toThrow( + BadRequestException, + ); + }); + }); + + describe('deleteFieldTemplateById', () => { + it('should delete field template', async () => { + const projectId = faker.number.int(); + const templateId = faker.number.int(); + + const mockTemplate = { + id: templateId, + projectId, + title: 'Test Template', + }; + + jest + .spyOn(aiFieldTemplatesRepo, 'findOne') + .mockResolvedValue(mockTemplate as any); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (aiFieldTemplatesRepo as any).delete = jest + .fn() + .mockResolvedValue({ affected: 1 }); + + await aiService.deleteFieldTemplateById(projectId, templateId); + + expect( + (aiFieldTemplatesRepo as unknown as { delete: jest.Mock }).delete, + ).toHaveBeenCalledWith(templateId); + }); + + it('should throw exception when deleting non-existent template', async () => { + const projectId = faker.number.int(); + const templateId = faker.number.int(); + + jest.spyOn(aiFieldTemplatesRepo, 'findOne').mockResolvedValue(null); + + await expect( + aiService.deleteFieldTemplateById(projectId, templateId), + ).rejects.toThrow(BadRequestException); + }); + }); + + describe('getModels', () => { + it('should return AI model list for project', async () => { + const projectId = faker.number.int(); + const mockIntegration = { + id: faker.number.int(), + projectId, + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + }; + + const mockModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo']; + + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue(mockIntegration as unknown as AIIntegrationsEntity); + + const mockClient = { + getModelList: jest.fn().mockResolvedValue(mockModels), + }; + jest.spyOn(await import('./ai.client'), 'AIClient').mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + () => mockClient as unknown as jest.MockedClass, + ); + + const result = await aiService.getModels(projectId); + + expect(result).toEqual(mockModels); + expect(mockClient.getModelList).toHaveBeenCalled(); + }); + + it('should return empty array when integration does not exist', async () => { + const projectId = faker.number.int(); + + jest.spyOn(aiIntegrationsRepo, 'findOne').mockResolvedValue(null); + + const result = await aiService.getModels(projectId); + + expect(result).toEqual([]); + }); + }); + + describe('getUsages', () => { + it('should retrieve AI usage for project', async () => { + const projectId = faker.number.int(); + const from = new Date('2024-01-01'); + const to = new Date('2024-01-31'); + + const mockUsages = [ + { + id: faker.number.int(), + projectId, + year: 2024, + month: 1, + day: 1, + category: UsageCategoryEnum.AI_FIELD, + provider: AIProvidersEnum.OPEN_AI, + usedTokens: 1000, + createdAt: from, + }, + { + id: faker.number.int(), + projectId, + year: 2024, + month: 1, + day: 2, + category: UsageCategoryEnum.ISSUE_RECOMMEND, + provider: AIProvidersEnum.OPEN_AI, + usedTokens: 500, + createdAt: to, + }, + ]; + + jest.spyOn(aiUsagesRepo, 'find').mockResolvedValue(mockUsages as any); + + const result = await aiService.getUsages(projectId, from, to); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + year: mockUsages[0].year, + month: mockUsages[0].month, + day: mockUsages[0].day, + category: mockUsages[0].category, + provider: mockUsages[0].provider, + usedTokens: mockUsages[0].usedTokens, + }); + }); + }); + + describe('recommendAIIssue', () => { + it('should recommend issues for feedback', async () => { + const feedbackId = faker.number.int(); + const projectId = faker.number.int(); + const channelId = faker.number.int(); + + const mockFeedback = { + id: feedbackId, + data: { + content: 'This is a test feedback', + rating: 5, + }, + channel: { + id: channelId, + project: { + id: projectId, + name: 'Test Project', + description: 'Test Description', + }, + }, + }; + + const mockIntegration = { + id: faker.number.int(), + projectId, + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + systemPrompt: 'You are a helpful assistant', + }; + + const mockIssueTemplate = { + id: faker.number.int(), + channelId, + targetFieldKeys: ['content', 'rating'], + prompt: 'Recommend issues based on feedback', + isEnabled: true, + model: 'gpt-4o', + temperature: 0.5, + dataReferenceAmount: 3, + }; + + const mockIssues = [ + { id: faker.number.int(), name: 'Bug Report', feedbackCount: 10 }, + { id: faker.number.int(), name: 'Feature Request', feedbackCount: 5 }, + ]; + + const mockClient = { + executeIssueRecommend: jest.fn().mockResolvedValue({ + status: AIPromptStatusEnum.success, + content: 'Bug Report, Feature Request', + usedTokens: 100, + }), + }; + + jest + .spyOn(feedbackRepo, 'findOne') + .mockResolvedValue(mockFeedback as any); + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue(mockIntegration as unknown as AIIntegrationsEntity); + jest + .spyOn(aiIssueTemplatesRepo, 'findOne') + .mockResolvedValue(mockIssueTemplate as any); + jest.spyOn(issueRepo, 'count').mockResolvedValue(10); + jest.spyOn(issueRepo, 'find').mockResolvedValue(mockIssues as any); + jest.spyOn(aiUsagesRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(aiUsagesRepo, 'save').mockResolvedValue({} as any); + jest.spyOn(await import('./ai.client'), 'AIClient').mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + () => mockClient as unknown as jest.MockedClass, + ); + + const result = await aiService.recommendAIIssue(feedbackId); + + expect(result.success).toBe(true); + expect(result.result).toHaveLength(2); + expect(result.result[0].issueName).toBe('Bug Report'); + expect(result.result[1].issueName).toBe('Feature Request'); + }); + + it('should throw exception when feedback does not exist', async () => { + const feedbackId = faker.number.int(); + + jest.spyOn(feedbackRepo, 'findOne').mockResolvedValue(null); + + await expect(aiService.recommendAIIssue(feedbackId)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should throw exception when integration does not exist', async () => { + const feedbackId = faker.number.int(); + const mockFeedback = { + id: feedbackId, + channel: { + project: { + id: faker.number.int(), + }, + }, + }; + + jest + .spyOn(feedbackRepo, 'findOne') + .mockResolvedValue(mockFeedback as any); + jest.spyOn(aiIntegrationsRepo, 'findOne').mockResolvedValue(null); + + await expect(aiService.recommendAIIssue(feedbackId)).rejects.toThrow( + BadRequestException, + ); + }); + + it('should throw exception when issue template does not exist', async () => { + const feedbackId = faker.number.int(); + const projectId = faker.number.int(); + const channelId = faker.number.int(); + + const mockFeedback = { + id: feedbackId, + channel: { + id: channelId, + project: { + id: projectId, + }, + }, + }; + + const mockIntegration = { + id: faker.number.int(), + projectId, + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + }; + + jest + .spyOn(feedbackRepo, 'findOne') + .mockResolvedValue(mockFeedback as any); + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue(mockIntegration as unknown as AIIntegrationsEntity); + jest.spyOn(aiIssueTemplatesRepo, 'findOne').mockResolvedValue(null); + + await expect(aiService.recommendAIIssue(feedbackId)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('addPermissions', () => { + it('should add AI permissions to roles', async () => { + const mockRoles = [ + { + id: faker.number.int(), + name: 'Admin', + permissions: ['project_read', 'project_update'], + }, + { + id: faker.number.int(), + name: 'Editor', + permissions: ['project_read'], + }, + { + id: faker.number.int(), + name: 'Viewer', + permissions: [], + }, + ]; + + jest.spyOn(roleRepo, 'find').mockResolvedValue(mockRoles as any); + jest.spyOn(roleRepo, 'save').mockResolvedValue({} as any); + + await aiService.addPermissions(); + + expect(roleRepo.save).toHaveBeenCalledTimes(2); + }); + }); + + describe('processFeedbackAIFields', () => { + it('should process AI fields for feedback', async () => { + const feedbackId = faker.number.int(); + const projectId = faker.number.int(); + const channelId = faker.number.int(); + + const mockFeedback = { + id: feedbackId, + data: { + content: 'Test feedback content', + rating: 5, + }, + channel: { + id: channelId, + project: { + id: projectId, + }, + }, + }; + + const mockFields = [ + { + id: faker.number.int(), + key: 'content', + format: FieldFormatEnum.text, + status: FieldStatusEnum.ACTIVE, + }, + { + id: faker.number.int(), + key: 'ai_summary', + format: FieldFormatEnum.aiField, + status: FieldStatusEnum.ACTIVE, + aiFieldTargetKeys: ['content'], + aiFieldTemplate: { + id: faker.number.int(), + model: 'gpt-4o', + temperature: 0.5, + prompt: 'Summarize the content', + }, + }, + ]; + + jest + .spyOn(feedbackRepo, 'findOne') + .mockResolvedValue(mockFeedback as any); + jest.spyOn(fieldRepo, 'find').mockResolvedValue(mockFields as any); + jest.spyOn(aiService, 'executeAIFieldPrompt').mockResolvedValue(true); + + await aiService.processFeedbackAIFields(feedbackId); + + expect(aiService.executeAIFieldPrompt).toHaveBeenCalledWith( + mockFeedback, + mockFields[1], + [mockFields[0]], + mockFields, + ); + }); + + it('should throw exception when feedback does not exist', async () => { + const feedbackId = faker.number.int(); + + jest.spyOn(feedbackRepo, 'findOne').mockResolvedValue(null); + + await expect( + aiService.processFeedbackAIFields(feedbackId), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('processAIField', () => { + it('should process specific AI field', async () => { + const feedbackId = faker.number.int(); + const fieldId = faker.number.int(); + const projectId = faker.number.int(); + const channelId = faker.number.int(); + + const mockFeedback = { + id: feedbackId, + data: { + content: 'Test feedback content', + }, + channel: { + id: channelId, + project: { + id: projectId, + }, + }, + }; + + const mockFields = [ + { + id: faker.number.int(), + key: 'content', + format: FieldFormatEnum.text, + }, + { + id: fieldId, + key: 'ai_summary', + format: FieldFormatEnum.aiField, + status: FieldStatusEnum.ACTIVE, + aiFieldTargetKeys: ['content'], + aiFieldTemplate: { + id: faker.number.int(), + model: 'gpt-4o', + temperature: 0.5, + prompt: 'Summarize the content', + }, + }, + ]; + + jest + .spyOn(feedbackRepo, 'findOne') + .mockResolvedValue(mockFeedback as any); + jest.spyOn(fieldRepo, 'find').mockResolvedValue(mockFields as any); + jest.spyOn(aiService, 'executeAIFieldPrompt').mockResolvedValue(true); + + await aiService.processAIField(feedbackId, fieldId); + + expect(aiService.executeAIFieldPrompt).toHaveBeenCalledWith( + mockFeedback, + mockFields[1], + [mockFields[0]], + mockFields, + ); + }); + + it('should throw exception when AI field does not exist', async () => { + const feedbackId = faker.number.int(); + const fieldId = faker.number.int(); + + const mockFeedback = { + id: feedbackId, + channel: { + id: faker.number.int(), + project: { + id: faker.number.int(), + }, + }, + }; + + const mockFields = [ + { + id: faker.number.int(), + key: 'content', + format: FieldFormatEnum.text, + }, + ]; + + jest + .spyOn(feedbackRepo, 'findOne') + .mockResolvedValue(mockFeedback as any); + jest.spyOn(fieldRepo, 'find').mockResolvedValue(mockFields as any); + + await expect( + aiService.processAIField(feedbackId, fieldId), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('getPlaygroundPromptResult', () => { + it('should return playground prompt result', async () => { + const projectId = faker.number.int(); + const dto = new GetAIPlaygroundResultDto(); + dto.projectId = projectId; + dto.model = 'gpt-4o'; + dto.temperature = 0.5; + dto.templatePrompt = 'Summarize the following content'; + dto.temporaryFields = [ + { + name: 'content', + description: 'User feedback content', + value: 'This is a test feedback', + }, + ]; + + const mockIntegration = { + id: faker.number.int(), + projectId, + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + systemPrompt: 'You are a helpful assistant', + }; + + const mockProject = { + id: projectId, + name: 'Test Project', + description: 'Test Description', + }; + + const mockClient = { + executePrompt: jest.fn().mockResolvedValue({ + status: AIPromptStatusEnum.success, + content: 'This is a summary of the feedback', + usedTokens: 50, + }), + }; + + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue(mockIntegration as unknown as AIIntegrationsEntity); + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValue(mockProject as any); + jest.spyOn(aiUsagesRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(aiUsagesRepo, 'save').mockResolvedValue({} as any); + jest.spyOn(await import('./ai.client'), 'AIClient').mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + () => mockClient as unknown as jest.MockedClass, + ); + + const result = await aiService.getPlaygroundPromptResult(dto); + + expect(result).toBe('This is a summary of the feedback'); + expect(mockClient.executePrompt).toHaveBeenCalled(); + }); + + it('should throw exception when integration does not exist', async () => { + const dto = new GetAIPlaygroundResultDto(); + dto.projectId = faker.number.int(); + + jest.spyOn(aiIntegrationsRepo, 'findOne').mockResolvedValue(null); + + await expect(aiService.getPlaygroundPromptResult(dto)).rejects.toThrow( + BadRequestException, + ); + }); + + it('should throw exception when project does not exist', async () => { + const projectId = faker.number.int(); + const dto = new GetAIPlaygroundResultDto(); + dto.projectId = projectId; + + const mockIntegration = { + id: faker.number.int(), + projectId, + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + }; + + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue(mockIntegration as unknown as AIIntegrationsEntity); + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(null); + + await expect(aiService.getPlaygroundPromptResult(dto)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('getIssuePlaygroundPromptResult', () => { + it('should return issue playground prompt result', async () => { + const channelId = faker.number.int(); + const projectId = faker.number.int(); + + const dto = new GetAIIssuePlaygroundResultDto(); + dto.channelId = channelId; + dto.model = 'gpt-4o'; + dto.temperature = 0.5; + dto.templatePrompt = 'Recommend issues based on feedback'; + dto.dataReferenceAmount = 3; + dto.temporaryFields = [ + { + name: 'content', + description: 'User feedback content', + value: 'This is a test feedback', + }, + ]; + + const mockChannel = { + id: channelId, + project: { + id: projectId, + name: 'Test Project', + description: 'Test Description', + }, + }; + + const mockIntegration = { + id: faker.number.int(), + projectId, + provider: AIProvidersEnum.OPEN_AI, + apiKey: faker.string.alphanumeric(32), + endpointUrl: faker.internet.url(), + systemPrompt: 'You are a helpful assistant', + }; + + const mockIssues = [ + { id: faker.number.int(), name: 'Bug Report', feedbackCount: 10 }, + { id: faker.number.int(), name: 'Feature Request', feedbackCount: 5 }, + ]; + + const mockClient = { + executeIssueRecommend: jest.fn().mockResolvedValue({ + status: AIPromptStatusEnum.success, + content: 'Bug Report, Feature Request', + usedTokens: 100, + }), + }; + + jest.spyOn(channelRepo, 'findOne').mockResolvedValue(mockChannel as any); + jest + .spyOn(aiIntegrationsRepo, 'findOne') + .mockResolvedValue(mockIntegration as unknown as AIIntegrationsEntity); + jest.spyOn(issueRepo, 'count').mockResolvedValue(10); + jest.spyOn(issueRepo, 'find').mockResolvedValue(mockIssues as any); + jest.spyOn(aiUsagesRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(aiUsagesRepo, 'save').mockResolvedValue({} as any); + jest.spyOn(await import('./ai.client'), 'AIClient').mockImplementation( + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + () => mockClient as unknown as jest.MockedClass, + ); + + const result = await aiService.getIssuePlaygroundPromptResult(dto); + + expect(result).toEqual(['Bug Report', ' Feature Request']); + expect(mockClient.executeIssueRecommend).toHaveBeenCalled(); + }); + + it('should throw exception when channel does not exist', async () => { + const dto = new GetAIIssuePlaygroundResultDto(); + dto.channelId = faker.number.int(); + + jest.spyOn(channelRepo, 'findOne').mockResolvedValue(null); + + await expect( + aiService.getIssuePlaygroundPromptResult(dto), + ).rejects.toThrow(NotFoundException); + }); + + it('should throw exception when integration does not exist', async () => { + const channelId = faker.number.int(); + const dto = new GetAIIssuePlaygroundResultDto(); + dto.channelId = channelId; + + const mockChannel = { + id: channelId, + project: { + id: faker.number.int(), + }, + }; + + jest.spyOn(channelRepo, 'findOne').mockResolvedValue(mockChannel as any); + jest.spyOn(aiIntegrationsRepo, 'findOne').mockResolvedValue(null); + + await expect( + aiService.getIssuePlaygroundPromptResult(dto), + ).rejects.toThrow(BadRequestException); + }); + }); + + describe('findIssueTemplatesByProjectId', () => { + it('should retrieve issue template list by project ID', async () => { + const projectId = faker.number.int(); + const channelId = faker.number.int(); + + const mockTemplates = [ + { + id: faker.number.int(), + channel: { + id: channelId, + }, + targetFieldKeys: ['content', 'rating'], + prompt: 'Recommend issues based on feedback', + isEnabled: true, + model: 'gpt-4o', + temperature: 0.5, + dataReferenceAmount: 3, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + jest + .spyOn(aiIssueTemplatesRepo, 'find') + .mockResolvedValue(mockTemplates as any); + + const result = await aiService.findIssueTemplatesByProjectId(projectId); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: mockTemplates[0].id, + channelId: mockTemplates[0].channel.id, + targetFieldKeys: mockTemplates[0].targetFieldKeys, + prompt: mockTemplates[0].prompt, + isEnabled: mockTemplates[0].isEnabled, + model: mockTemplates[0].model, + temperature: mockTemplates[0].temperature, + dataReferenceAmount: mockTemplates[0].dataReferenceAmount, + createdAt: mockTemplates[0].createdAt, + updatedAt: mockTemplates[0].updatedAt, + }); + }); + }); + + describe('createNewIssueTemplate', () => { + it('should create new issue template', async () => { + const dto = new CreateAIIssueTemplateDto(); + dto.channelId = faker.number.int(); + dto.targetFieldKeys = ['content', 'rating']; + dto.prompt = 'Recommend issues based on feedback'; + dto.isEnabled = true; + dto.model = 'gpt-4o'; + dto.temperature = 0.5; + dto.dataReferenceAmount = 3; + + const mockTemplate = { + id: faker.number.int(), + ...dto, + createdAt: new Date(), + updatedAt: new Date(), + }; + + jest + .spyOn(aiIssueTemplatesRepo, 'save') + .mockResolvedValue(mockTemplate as any); + + await aiService.createNewIssueTemplate(dto); + + expect(aiIssueTemplatesRepo.save).toHaveBeenCalled(); + }); + }); + + describe('updateIssueTemplate', () => { + it('should update existing issue template', async () => { + const dto = new UpdateAIIssueTemplateDto(); + dto.templateId = faker.number.int(); + dto.channelId = faker.number.int(); + dto.prompt = 'Updated prompt'; + dto.isEnabled = false; + + const existingTemplate = { + id: dto.templateId, + channelId: dto.channelId, + prompt: 'Old prompt', + isEnabled: true, + model: 'gpt-4o', + temperature: 0.5, + dataReferenceAmount: 3, + }; + + jest + .spyOn(aiIssueTemplatesRepo, 'findOne') + .mockResolvedValue(existingTemplate as any); + jest + .spyOn(aiIssueTemplatesRepo, 'save') + .mockResolvedValue({ ...existingTemplate, ...dto } as any); + + await aiService.updateIssueTemplate(dto); + + expect(aiIssueTemplatesRepo.save).toHaveBeenCalledWith({ + ...existingTemplate, + ...dto, + }); + }); + + it('should throw exception when updating non-existent template', async () => { + const dto = new UpdateAIIssueTemplateDto(); + dto.templateId = faker.number.int(); + dto.channelId = faker.number.int(); + + jest.spyOn(aiIssueTemplatesRepo, 'findOne').mockResolvedValue(null); + + await expect(aiService.updateIssueTemplate(dto)).rejects.toThrow( + BadRequestException, + ); + }); + }); + + describe('deleteIssueTemplateById', () => { + it('should delete issue template', async () => { + const templateId = faker.number.int(); + + const mockTemplate = { + id: templateId, + prompt: 'Test Template', + }; + + jest + .spyOn(aiIssueTemplatesRepo, 'findOne') + .mockResolvedValue(mockTemplate as any); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (aiIssueTemplatesRepo as any).delete = jest + .fn() + .mockResolvedValue({ affected: 1 }); + + await aiService.deleteIssueTemplateById(templateId); + + expect( + (aiIssueTemplatesRepo as unknown as { delete: jest.Mock }).delete, + ).toHaveBeenCalledWith(templateId); + }); + + it('should throw exception when deleting non-existent template', async () => { + const templateId = faker.number.int(); + + jest.spyOn(aiIssueTemplatesRepo, 'findOne').mockResolvedValue(null); + + await expect( + aiService.deleteIssueTemplateById(templateId), + ).rejects.toThrow(BadRequestException); + }); + }); + + describe('processFeedbacksAIFields', () => { + it('should process AI fields for multiple feedbacks', async () => { + const feedbackIds = [faker.number.int(), faker.number.int()]; + + jest + .spyOn(aiService, 'processFeedbackAIFields') + .mockResolvedValue(undefined); + + await aiService.processFeedbacksAIFields(feedbackIds); + + expect(aiService.processFeedbackAIFields).toHaveBeenCalledTimes(2); + expect(aiService.processFeedbackAIFields).toHaveBeenCalledWith( + feedbackIds[0], + ); + expect(aiService.processFeedbackAIFields).toHaveBeenCalledWith( + feedbackIds[1], + ); + }); + + it('should throw exception when processing fails', async () => { + const feedbackIds = [faker.number.int()]; + + jest + .spyOn(aiService, 'processFeedbackAIFields') + .mockRejectedValue(new Error('Processing failed')); + + await expect( + aiService.processFeedbacksAIFields(feedbackIds), + ).rejects.toThrow(BadRequestException); + }); + }); +}); diff --git a/apps/api/src/domains/admin/project/api-key/api-key.controller.spec.ts b/apps/api/src/domains/admin/project/api-key/api-key.controller.spec.ts index 6d36657ac..0a37ad152 100644 --- a/apps/api/src/domains/admin/project/api-key/api-key.controller.spec.ts +++ b/apps/api/src/domains/admin/project/api-key/api-key.controller.spec.ts @@ -14,12 +14,15 @@ * under the License. */ import { faker } from '@faker-js/faker'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { DataSource } from 'typeorm'; import { getMockProvider, MockDataSource } from '@/test-utils/util-functions'; import { ApiKeyController } from './api-key.controller'; import { ApiKeyService } from './api-key.service'; +import { CreateApiKeyResponseDto } from './dtos/responses/create-api-key-response.dto'; +import { FindApiKeysResponseDto } from './dtos/responses/find-api-keys-response.dto'; const MockApiKeyService = { create: jest.fn(), @@ -45,63 +48,324 @@ describe('ApiKeyController', () => { apiKeyController = module.get(ApiKeyController); }); - describe('create ', () => { - it('creating succeeds without an api key', async () => { - jest.spyOn(MockApiKeyService, 'create'); + describe('create', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create API key successfully without providing value', async () => { + const projectId = faker.number.int(); + const mockApiKey = { + id: faker.number.int(), + value: faker.string.alphanumeric(20), + createdAt: faker.date.recent(), + }; + + MockApiKeyService.create.mockResolvedValue(mockApiKey); + + const result = await apiKeyController.create(projectId, {}); + + expect(MockApiKeyService.create).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.create).toHaveBeenCalledWith( + expect.objectContaining({ + projectId, + value: undefined, + }), + ); + expect(result).toBeInstanceOf(CreateApiKeyResponseDto); + expect(result.id).toBe(mockApiKey.id); + expect(result.value).toBe(mockApiKey.value); + expect(result.createdAt).toStrictEqual(mockApiKey.createdAt); + }); + + it('should create API key successfully with provided value', async () => { + const projectId = faker.number.int(); + const value = faker.string.alphanumeric(20); + const mockApiKey = { + id: faker.number.int(), + value, + createdAt: faker.date.recent(), + }; + + MockApiKeyService.create.mockResolvedValue(mockApiKey); + + const result = await apiKeyController.create(projectId, { value }); + + expect(MockApiKeyService.create).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.create).toHaveBeenCalledWith( + expect.objectContaining({ + projectId, + value, + }), + ); + expect(result).toBeInstanceOf(CreateApiKeyResponseDto); + expect(result.id).toBe(mockApiKey.id); + expect(result.value).toBe(mockApiKey.value); + expect(result.createdAt).toStrictEqual(mockApiKey.createdAt); + }); + + it('should throw BadRequestException when API key value is invalid length', async () => { const projectId = faker.number.int(); + const value = faker.string.alphanumeric(15); // Invalid length + const errorMessage = 'Invalid Api Key value'; - await apiKeyController.create(projectId, {}); + MockApiKeyService.create.mockRejectedValue( + new BadRequestException(errorMessage), + ); + await expect( + apiKeyController.create(projectId, { value }), + ).rejects.toThrow(BadRequestException); expect(MockApiKeyService.create).toHaveBeenCalledTimes(1); }); - it('creating succeeds with an api key', async () => { - jest.spyOn(MockApiKeyService, 'create'); + + it('should throw BadRequestException when API key already exists', async () => { const projectId = faker.number.int(); const value = faker.string.alphanumeric(20); + const errorMessage = 'Api Key already exists'; - await apiKeyController.create(projectId, { value }); + MockApiKeyService.create.mockRejectedValue( + new BadRequestException(errorMessage), + ); + await expect( + apiKeyController.create(projectId, { value }), + ).rejects.toThrow(BadRequestException); + expect(MockApiKeyService.create).toHaveBeenCalledTimes(1); + }); + + it('should throw NotFoundException when project does not exist', async () => { + const projectId = faker.number.int(); + const value = faker.string.alphanumeric(20); + + MockApiKeyService.create.mockRejectedValue( + new NotFoundException('Project not found'), + ); + + await expect( + apiKeyController.create(projectId, { value }), + ).rejects.toThrow(NotFoundException); + expect(MockApiKeyService.create).toHaveBeenCalledTimes(1); + }); + + it('should propagate other exceptions from service', async () => { + const projectId = faker.number.int(); + const error = new Error('Database connection failed'); + + MockApiKeyService.create.mockRejectedValue(error); + + await expect(apiKeyController.create(projectId, {})).rejects.toThrow( + error, + ); expect(MockApiKeyService.create).toHaveBeenCalledTimes(1); }); }); describe('findAll', () => { - it('', async () => { - jest.spyOn(MockApiKeyService, 'findAllByProjectId'); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return all API keys for a project successfully', async () => { const projectId = faker.number.int(); + const mockApiKeys = [ + { + id: faker.number.int(), + value: faker.string.alphanumeric(20), + createdAt: faker.date.recent(), + deletedAt: null, + }, + { + id: faker.number.int(), + value: faker.string.alphanumeric(20), + createdAt: faker.date.recent(), + deletedAt: faker.date.recent(), + }, + ]; - await apiKeyController.findAll(projectId); + MockApiKeyService.findAllByProjectId.mockResolvedValue(mockApiKeys); + + const result = await apiKeyController.findAll(projectId); expect(MockApiKeyService.findAllByProjectId).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.findAllByProjectId).toHaveBeenCalledWith( + projectId, + ); + expect(result).toBeInstanceOf(FindApiKeysResponseDto); + expect(result.items).toHaveLength(2); + expect(result.items[0].id).toBe(mockApiKeys[0].id); + expect(result.items[0].value).toBe(mockApiKeys[0].value); + expect(result.items[1].id).toBe(mockApiKeys[1].id); + expect(result.items[1].value).toBe(mockApiKeys[1].value); + }); + + it('should return empty array when no API keys exist for project', async () => { + const projectId = faker.number.int(); + + MockApiKeyService.findAllByProjectId.mockResolvedValue([]); + + const result = await apiKeyController.findAll(projectId); + + expect(MockApiKeyService.findAllByProjectId).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.findAllByProjectId).toHaveBeenCalledWith( + projectId, + ); + expect(result).toBeInstanceOf(FindApiKeysResponseDto); + expect(result.items).toHaveLength(0); + }); + + it('should propagate exceptions from service', async () => { + const projectId = faker.number.int(); + const error = new Error('Database connection failed'); + + MockApiKeyService.findAllByProjectId.mockRejectedValue(error); + + await expect(apiKeyController.findAll(projectId)).rejects.toThrow(error); + expect(MockApiKeyService.findAllByProjectId).toHaveBeenCalledTimes(1); }); }); describe('softDelete', () => { - it('', async () => { - jest.spyOn(MockApiKeyService, 'softDeleteById'); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should soft delete API key successfully', async () => { const apiKeyId = faker.number.int(); + MockApiKeyService.softDeleteById.mockResolvedValue(undefined); + await apiKeyController.softDelete(apiKeyId); expect(MockApiKeyService.softDeleteById).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.softDeleteById).toHaveBeenCalledWith(apiKeyId); + }); + + it('should propagate exceptions from service', async () => { + const apiKeyId = faker.number.int(); + const error = new Error('Database connection failed'); + + MockApiKeyService.softDeleteById.mockRejectedValue(error); + + await expect(apiKeyController.softDelete(apiKeyId)).rejects.toThrow( + error, + ); + expect(MockApiKeyService.softDeleteById).toHaveBeenCalledTimes(1); }); }); + describe('recover', () => { - it('', async () => { - jest.spyOn(MockApiKeyService, 'recoverById'); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should recover soft deleted API key successfully', async () => { const apiKeyId = faker.number.int(); + MockApiKeyService.recoverById.mockResolvedValue(undefined); + await apiKeyController.recover(apiKeyId); expect(MockApiKeyService.recoverById).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.recoverById).toHaveBeenCalledWith(apiKeyId); + }); + + it('should propagate exceptions from service', async () => { + const apiKeyId = faker.number.int(); + const error = new Error('Database connection failed'); + + MockApiKeyService.recoverById.mockRejectedValue(error); + + await expect(apiKeyController.recover(apiKeyId)).rejects.toThrow(error); + expect(MockApiKeyService.recoverById).toHaveBeenCalledTimes(1); }); }); + describe('delete', () => { - it('', async () => { - jest.spyOn(MockApiKeyService, 'deleteById'); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should permanently delete API key successfully', async () => { const apiKeyId = faker.number.int(); + MockApiKeyService.deleteById.mockResolvedValue(undefined); + await apiKeyController.delete(apiKeyId); expect(MockApiKeyService.deleteById).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.deleteById).toHaveBeenCalledWith(apiKeyId); + }); + + it('should propagate exceptions from service', async () => { + const apiKeyId = faker.number.int(); + const error = new Error('Database connection failed'); + + MockApiKeyService.deleteById.mockRejectedValue(error); + + await expect(apiKeyController.delete(apiKeyId)).rejects.toThrow(error); + expect(MockApiKeyService.deleteById).toHaveBeenCalledTimes(1); + }); + }); + + describe('Integration Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle complete API key lifecycle', async () => { + const projectId = faker.number.int(); + const apiKeyId = faker.number.int(); + const mockApiKey = { + id: apiKeyId, + value: faker.string.alphanumeric(20), + createdAt: faker.date.recent(), + }; + + // Create API key + MockApiKeyService.create.mockResolvedValue(mockApiKey); + const createResult = await apiKeyController.create(projectId, {}); + expect(createResult.id).toBe(apiKeyId); + + // Find all API keys + MockApiKeyService.findAllByProjectId.mockResolvedValue([mockApiKey]); + const findAllResult = await apiKeyController.findAll(projectId); + expect(findAllResult.items).toHaveLength(1); + + // Soft delete API key + MockApiKeyService.softDeleteById.mockResolvedValue(undefined); + await apiKeyController.softDelete(apiKeyId); + expect(MockApiKeyService.softDeleteById).toHaveBeenCalledWith(apiKeyId); + + // Recover API key + MockApiKeyService.recoverById.mockResolvedValue(undefined); + await apiKeyController.recover(apiKeyId); + expect(MockApiKeyService.recoverById).toHaveBeenCalledWith(apiKeyId); + + // Permanently delete API key + MockApiKeyService.deleteById.mockResolvedValue(undefined); + await apiKeyController.delete(apiKeyId); + expect(MockApiKeyService.deleteById).toHaveBeenCalledWith(apiKeyId); + }); + + it('should handle concurrent operations gracefully', async () => { + const projectId = faker.number.int(); + const apiKeyId = faker.number.int(); + + // Simulate concurrent operations + const operations = [ + apiKeyController.findAll(projectId), + apiKeyController.softDelete(apiKeyId), + apiKeyController.recover(apiKeyId), + ]; + + MockApiKeyService.findAllByProjectId.mockResolvedValue([]); + MockApiKeyService.softDeleteById.mockResolvedValue(undefined); + MockApiKeyService.recoverById.mockResolvedValue(undefined); + + await Promise.all(operations); + + expect(MockApiKeyService.findAllByProjectId).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.softDeleteById).toHaveBeenCalledTimes(1); + expect(MockApiKeyService.recoverById).toHaveBeenCalledTimes(1); }); }); }); diff --git a/apps/api/src/domains/admin/project/api-key/api-key.service.spec.ts b/apps/api/src/domains/admin/project/api-key/api-key.service.spec.ts index c3ae46e0d..1f05cd39c 100644 --- a/apps/api/src/domains/admin/project/api-key/api-key.service.spec.ts +++ b/apps/api/src/domains/admin/project/api-key/api-key.service.spec.ts @@ -46,23 +46,47 @@ describe('ApiKeyService', () => { describe('create', () => { it('creating an api key succeeds with a valid project id', async () => { const projectId = faker.number.int(); + const mockProject = { id: projectId, name: faker.company.name() }; + const mockApiKey = { + id: faker.number.int(), + value: faker.string.alphanumeric(20), + projectId, + }; + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValueOnce(mockProject as ProjectEntity); jest.spyOn(apiKeyRepo, 'findOneBy').mockResolvedValueOnce(null); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValueOnce(mockApiKey as any); const apiKey = await apiKeyService.create( CreateApiKeyDto.from({ projectId }), ); expect(apiKey.value).toHaveLength(20); + expect(projectRepo.findOneBy).toHaveBeenCalledWith({ id: projectId }); }); + it('creating an api key succeeds with a valid project id and a key', async () => { const projectId = faker.number.int(); const value = faker.string.alphanumeric(20); + const mockProject = { id: projectId, name: faker.company.name() }; + const mockApiKey = { + id: faker.number.int(), + value, + projectId, + }; + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValueOnce(mockProject as ProjectEntity); jest.spyOn(apiKeyRepo, 'findOneBy').mockResolvedValueOnce(null); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValueOnce(mockApiKey as any); const apiKey = await apiKeyService.create({ projectId, value }); expect(apiKey.value).toHaveLength(20); + expect(projectRepo.findOneBy).toHaveBeenCalledWith({ id: projectId }); }); + it('creating an api key fails with an invalid project id', async () => { const invalidProjectId = faker.number.int(); jest.spyOn(projectRepo, 'findOneBy').mockResolvedValueOnce(null); @@ -73,7 +97,8 @@ describe('ApiKeyService', () => { ), ).rejects.toThrow(ProjectNotFoundException); }); - it('creating an api key fails with an invalid api key', async () => { + + it('creating an api key fails with an invalid api key length', async () => { const projectId = faker.number.int(); const value = faker.string.alphanumeric( faker.number.int({ min: 1, max: 19 }), @@ -83,14 +108,33 @@ describe('ApiKeyService', () => { new BadRequestException('Invalid Api Key value'), ); }); + it('creating an api key fails with an existent api key', async () => { const projectId = faker.number.int(); const value = faker.string.alphanumeric(20); + const mockProject = { id: projectId, name: faker.company.name() }; + const existingApiKey = { id: 1, value, projectId }; + + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValueOnce(mockProject as ProjectEntity); + jest + .spyOn(apiKeyRepo, 'findOneBy') + .mockResolvedValueOnce(existingApiKey as any); await expect(apiKeyService.create({ projectId, value })).rejects.toThrow( new BadRequestException('Api Key already exists'), ); }); + + it('creating an api key fails with api key longer than 20 characters', async () => { + const projectId = faker.number.int(); + const value = faker.string.alphanumeric(21); + + await expect(apiKeyService.create({ projectId, value })).rejects.toThrow( + new BadRequestException('Invalid Api Key value'), + ); + }); }); describe('createMany', () => { @@ -105,7 +149,17 @@ describe('ApiKeyService', () => { }); it('creating api keys succeeds with a valid project id', async () => { + const mockProject = { id: projectId, name: faker.company.name() }; + const mockApiKeys = dtos.map((_) => ({ + id: faker.number.int(), + value: faker.string.alphanumeric(20), + projectId, + })); + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValue(mockProject as ProjectEntity); jest.spyOn(apiKeyRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValue(mockApiKeys as any); const apiKeys = await apiKeyService.createMany(dtos); @@ -113,12 +167,24 @@ describe('ApiKeyService', () => { for (const apiKey of apiKeys) { expect(apiKey.value).toHaveLength(20); } + expect(projectRepo.findOneBy).toHaveBeenCalledTimes(apiKeyCount); }); + it('creating api keys succeeds with a valid project id and keys', async () => { + const mockProject = { id: projectId, name: faker.company.name() }; dtos.forEach((apiKey) => { apiKey.value = faker.string.alphanumeric(20); }); + const mockApiKeys = dtos.map((dto) => ({ + id: faker.number.int(), + value: dto.value, + projectId, + })); + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValue(mockProject as ProjectEntity); jest.spyOn(apiKeyRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValue(mockApiKeys as any); const apiKeys = await apiKeyService.createMany(dtos); @@ -127,14 +193,206 @@ describe('ApiKeyService', () => { expect(apiKey.value).toHaveLength(20); } }); + it('creating api keys fails with an invalid project id', async () => { const invalidProjectId = faker.number.int(); dtos[0].projectId = invalidProjectId; - jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValueOnce(null); await expect(apiKeyService.createMany(dtos)).rejects.toThrow( ProjectNotFoundException, ); }); + + it('creating api keys fails with duplicate api key values', async () => { + const duplicateValue = faker.string.alphanumeric(20); + const mockProject = { id: projectId, name: faker.company.name() }; + dtos.forEach((apiKey) => { + apiKey.value = duplicateValue; + }); + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValue(mockProject as ProjectEntity); + jest + .spyOn(apiKeyRepo, 'findOneBy') + .mockResolvedValueOnce({} as ApiKeyEntity); + + await expect(apiKeyService.createMany(dtos)).rejects.toThrow( + new BadRequestException('Api Key already exists'), + ); + }); + + it('creating api keys fails with invalid api key length', async () => { + const mockProject = { id: projectId, name: faker.company.name() }; + dtos[0].value = faker.string.alphanumeric(19); // Invalid length + jest + .spyOn(projectRepo, 'findOneBy') + .mockResolvedValue(mockProject as ProjectEntity); + + await expect(apiKeyService.createMany(dtos)).rejects.toThrow( + new BadRequestException('Invalid Api Key value'), + ); + }); + }); + + describe('findAllByProjectId', () => { + it('returns all api keys for a valid project id', async () => { + const projectId = faker.number.int(); + const mockApiKeys = [ + { id: 1, value: faker.string.alphanumeric(20), projectId }, + { id: 2, value: faker.string.alphanumeric(20), projectId }, + ]; + jest.spyOn(apiKeyRepo, 'find').mockResolvedValueOnce(mockApiKeys as any); + + const result = await apiKeyService.findAllByProjectId(projectId); + + expect(result).toEqual(mockApiKeys); + expect(apiKeyRepo.find).toHaveBeenCalledWith({ + where: { project: { id: projectId } }, + withDeleted: true, + }); + }); + + it('returns empty array when no api keys exist for project', async () => { + const projectId = faker.number.int(); + jest.spyOn(apiKeyRepo, 'find').mockResolvedValueOnce([]); + + const result = await apiKeyService.findAllByProjectId(projectId); + + expect(result).toEqual([]); + }); + }); + + describe('findByProjectIdAndValue', () => { + it('returns api keys matching project id and value', async () => { + const projectId = faker.number.int(); + const value = faker.string.alphanumeric(20); + const mockApiKeys = [{ id: 1, value, projectId }]; + jest.spyOn(apiKeyRepo, 'find').mockResolvedValueOnce(mockApiKeys as any); + + const result = await apiKeyService.findByProjectIdAndValue( + projectId, + value, + ); + + expect(result).toEqual(mockApiKeys); + expect(apiKeyRepo.find).toHaveBeenCalledWith({ + where: { project: { id: projectId }, value }, + }); + }); + + it('returns empty array when no matching api keys found', async () => { + const projectId = faker.number.int(); + const value = faker.string.alphanumeric(20); + jest.spyOn(apiKeyRepo, 'find').mockResolvedValueOnce([]); + + const result = await apiKeyService.findByProjectIdAndValue( + projectId, + value, + ); + + expect(result).toEqual([]); + }); + }); + + describe('deleteById', () => { + it('deletes api key by id successfully', async () => { + const id = faker.number.int(); + jest + .spyOn(apiKeyRepo, 'remove') + .mockResolvedValueOnce({} as ApiKeyEntity); + + await apiKeyService.deleteById(id); + + expect(apiKeyRepo.remove).toHaveBeenCalledWith( + expect.objectContaining({ id }), + ); + }); + }); + + describe('softDeleteById', () => { + it('soft deletes api key by id when api key exists', async () => { + const id = faker.number.int(); + const existingApiKey = { id, value: faker.string.alphanumeric(20) }; + jest + .spyOn(apiKeyRepo, 'findOne') + .mockResolvedValueOnce(existingApiKey as any); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValueOnce({} as ApiKeyEntity); + + await apiKeyService.softDeleteById(id); + + expect(apiKeyRepo.findOne).toHaveBeenCalledWith({ + where: { id }, + }); + expect(apiKeyRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + ...existingApiKey, + deletedAt: expect.any(Date), + }), + ); + }); + + it('soft deletes api key by id when api key does not exist', async () => { + const id = faker.number.int(); + jest.spyOn(apiKeyRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValueOnce({} as ApiKeyEntity); + + await apiKeyService.softDeleteById(id); + + expect(apiKeyRepo.findOne).toHaveBeenCalledWith({ + where: { id }, + }); + expect(apiKeyRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + deletedAt: expect.any(Date), + }), + ); + }); + }); + + describe('recoverById', () => { + it('recovers soft deleted api key by id when api key exists', async () => { + const id = faker.number.int(); + const existingApiKey = { + id, + value: faker.string.alphanumeric(20), + deletedAt: new Date(), + }; + jest + .spyOn(apiKeyRepo, 'findOne') + .mockResolvedValueOnce(existingApiKey as any); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValueOnce({} as ApiKeyEntity); + + await apiKeyService.recoverById(id); + + expect(apiKeyRepo.findOne).toHaveBeenCalledWith({ + where: { id }, + withDeleted: true, + }); + expect(apiKeyRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + ...existingApiKey, + deletedAt: null, + }), + ); + }); + + it('recovers api key by id when api key does not exist', async () => { + const id = faker.number.int(); + jest.spyOn(apiKeyRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(apiKeyRepo, 'save').mockResolvedValueOnce({} as ApiKeyEntity); + + await apiKeyService.recoverById(id); + + expect(apiKeyRepo.findOne).toHaveBeenCalledWith({ + where: { id }, + withDeleted: true, + }); + expect(apiKeyRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + deletedAt: null, + }), + ); + }); }); }); diff --git a/apps/api/src/domains/admin/project/category/category.controller.spec.ts b/apps/api/src/domains/admin/project/category/category.controller.spec.ts new file mode 100644 index 000000000..d8a74760f --- /dev/null +++ b/apps/api/src/domains/admin/project/category/category.controller.spec.ts @@ -0,0 +1,362 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import { BadRequestException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; + +import { SortMethodEnum } from '@/common/enums'; +import { getMockProvider, MockDataSource } from '@/test-utils/util-functions'; +import { CategoryController } from './category.controller'; +import { CategoryService } from './category.service'; +import { CreateCategoryRequestDto } from './dtos/requests'; +import { GetAllCategoriesRequestDto } from './dtos/requests/get-all-categories-request.dto'; +import { UpdateCategoryRequestDto } from './dtos/requests/update-category-request.dto'; +import { CreateCategoryResponseDto } from './dtos/responses'; +import { GetAllCategoriesResponseDto } from './dtos/responses/get-all-categories-response.dto'; + +const MockCategoryService = { + create: jest.fn(), + findAllByProjectId: jest.fn(), + update: jest.fn(), + delete: jest.fn(), +}; + +describe('CategoryController', () => { + let categoryController: CategoryController; + + beforeEach(async () => { + // Reset all mocks before each test + jest.clearAllMocks(); + + const module = await Test.createTestingModule({ + controllers: [CategoryController], + providers: [ + getMockProvider(CategoryService, MockCategoryService), + getMockProvider(DataSource, MockDataSource), + ], + }).compile(); + + categoryController = module.get(CategoryController); + }); + + describe('create', () => { + it('should create category successfully', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + const body = new CreateCategoryRequestDto(); + body.name = faker.string.sample(); + + const mockCategory = { + id: categoryId, + name: body.name, + projectId, + createdAt: new Date(), + updatedAt: new Date(), + }; + + jest.spyOn(MockCategoryService, 'create').mockResolvedValue(mockCategory); + + const result = await categoryController.create(projectId, body); + + expect(MockCategoryService.create).toHaveBeenCalledWith({ + projectId, + name: body.name, + }); + expect(result).toBeInstanceOf(CreateCategoryResponseDto); + expect(result.id).toBe(categoryId); + }); + + it('should handle service errors in create', async () => { + const projectId = faker.number.int(); + const body = new CreateCategoryRequestDto(); + body.name = faker.string.sample(); + + jest + .spyOn(MockCategoryService, 'create') + .mockRejectedValue( + new BadRequestException('Category name already exists'), + ); + + await expect(categoryController.create(projectId, body)).rejects.toThrow( + 'Category name already exists', + ); + + expect(MockCategoryService.create).toHaveBeenCalledWith({ + projectId, + name: body.name, + }); + }); + }); + + describe('findAll', () => { + it('should find all categories successfully', async () => { + const projectId = faker.number.int(); + const body = new GetAllCategoriesRequestDto(); + body.page = faker.number.int({ min: 1, max: 10 }); + body.limit = faker.number.int({ min: 1, max: 100 }); + body.categoryName = faker.string.sample(); + body.sort = { name: SortMethodEnum.ASC }; + + const mockResult = { + items: [ + { + id: faker.number.int(), + name: faker.string.sample(), + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + meta: { + totalItems: 1, + itemCount: 1, + itemsPerPage: body.limit, + totalPages: 1, + currentPage: body.page, + }, + }; + + jest + .spyOn(MockCategoryService, 'findAllByProjectId') + .mockResolvedValue(mockResult); + + const result = await categoryController.findAll(projectId, body); + + expect(MockCategoryService.findAllByProjectId).toHaveBeenCalledWith({ + projectId, + page: body.page, + limit: body.limit, + categoryName: body.categoryName, + sort: body.sort, + }); + expect(result).toBeInstanceOf(GetAllCategoriesResponseDto); + expect(result.items).toHaveLength(1); + }); + + it('should find all categories with default pagination', async () => { + const projectId = faker.number.int(); + const body = new GetAllCategoriesRequestDto(); + body.page = 1; + body.limit = 10; + + const mockResult = { + items: [], + meta: { + totalItems: 0, + itemCount: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 1, + }, + }; + + jest + .spyOn(MockCategoryService, 'findAllByProjectId') + .mockResolvedValue(mockResult); + + const result = await categoryController.findAll(projectId, body); + + expect(MockCategoryService.findAllByProjectId).toHaveBeenCalledWith({ + projectId, + page: 1, + limit: 10, + categoryName: undefined, + sort: undefined, + }); + expect(result).toBeInstanceOf(GetAllCategoriesResponseDto); + expect(result.items).toHaveLength(0); + }); + + it('should handle service errors in findAll', async () => { + const projectId = faker.number.int(); + const body = new GetAllCategoriesRequestDto(); + body.page = 1; + body.limit = 10; + + jest + .spyOn(MockCategoryService, 'findAllByProjectId') + .mockRejectedValue(new BadRequestException('Invalid sort method')); + + await expect(categoryController.findAll(projectId, body)).rejects.toThrow( + 'Invalid sort method', + ); + + expect(MockCategoryService.findAllByProjectId).toHaveBeenCalledWith({ + projectId, + page: 1, + limit: 10, + categoryName: undefined, + sort: undefined, + }); + }); + }); + + describe('update', () => { + it('should update category successfully', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + const body = new UpdateCategoryRequestDto(); + body.name = faker.string.sample(); + + jest.spyOn(MockCategoryService, 'update').mockResolvedValue(undefined); + + await categoryController.update(projectId, categoryId, body); + + expect(MockCategoryService.update).toHaveBeenCalledWith({ + categoryId, + projectId, + name: body.name, + }); + }); + + it('should handle service errors in update', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + const body = new UpdateCategoryRequestDto(); + body.name = faker.string.sample(); + + jest + .spyOn(MockCategoryService, 'update') + .mockRejectedValue(new BadRequestException('Category not found')); + + await expect( + categoryController.update(projectId, categoryId, body), + ).rejects.toThrow('Category not found'); + + expect(MockCategoryService.update).toHaveBeenCalledWith({ + categoryId, + projectId, + name: body.name, + }); + }); + }); + + describe('delete', () => { + it('should delete category successfully', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + + jest.spyOn(MockCategoryService, 'delete').mockResolvedValue(undefined); + + await categoryController.delete(projectId, categoryId); + + expect(MockCategoryService.delete).toHaveBeenCalledWith({ + categoryId, + projectId, + }); + }); + + it('should handle service errors in delete', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + + jest + .spyOn(MockCategoryService, 'delete') + .mockRejectedValue(new BadRequestException('Category not found')); + + await expect( + categoryController.delete(projectId, categoryId), + ).rejects.toThrow('Category not found'); + + expect(MockCategoryService.delete).toHaveBeenCalledWith({ + categoryId, + projectId, + }); + }); + }); + + describe('Error Cases', () => { + it('should handle validation errors in create', async () => { + const projectId = faker.number.int(); + const body = new CreateCategoryRequestDto(); + body.name = ''; // Invalid empty name + + jest + .spyOn(MockCategoryService, 'create') + .mockRejectedValue(new BadRequestException('Name is required')); + + await expect(categoryController.create(projectId, body)).rejects.toThrow( + 'Name is required', + ); + }); + + it('should handle validation errors in update', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + const body = new UpdateCategoryRequestDto(); + body.name = 'a'.repeat(256); // Invalid too long name + + jest + .spyOn(MockCategoryService, 'update') + .mockRejectedValue(new BadRequestException('Name is too long')); + + await expect( + categoryController.update(projectId, categoryId, body), + ).rejects.toThrow('Name is too long'); + }); + + it('should handle database errors in findAll', async () => { + const projectId = faker.number.int(); + const body = new GetAllCategoriesRequestDto(); + body.page = 1; + body.limit = 10; + + jest + .spyOn(MockCategoryService, 'findAllByProjectId') + .mockRejectedValue( + new BadRequestException('Database connection error'), + ); + + await expect(categoryController.findAll(projectId, body)).rejects.toThrow( + 'Database connection error', + ); + }); + + it('should handle concurrent modification errors in update', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + const body = new UpdateCategoryRequestDto(); + body.name = faker.string.sample(); + + jest + .spyOn(MockCategoryService, 'update') + .mockRejectedValue( + new BadRequestException('Category was modified by another user'), + ); + + await expect( + categoryController.update(projectId, categoryId, body), + ).rejects.toThrow('Category was modified by another user'); + }); + + it('should handle foreign key constraint errors in delete', async () => { + const projectId = faker.number.int(); + const categoryId = faker.number.int(); + + jest + .spyOn(MockCategoryService, 'delete') + .mockRejectedValue( + new BadRequestException( + 'Cannot delete category with associated issues', + ), + ); + + await expect( + categoryController.delete(projectId, categoryId), + ).rejects.toThrow('Cannot delete category with associated issues'); + }); + }); +}); diff --git a/apps/api/src/domains/admin/project/category/category.service.spec.ts b/apps/api/src/domains/admin/project/category/category.service.spec.ts new file mode 100644 index 000000000..7488e9b19 --- /dev/null +++ b/apps/api/src/domains/admin/project/category/category.service.spec.ts @@ -0,0 +1,385 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { faker } from '@faker-js/faker'; +import { BadRequestException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { ClsModule } from 'nestjs-cls'; +import type { Repository } from 'typeorm'; + +import { SortMethodEnum } from '@/common/enums'; +import { CategoryEntity } from './category.entity'; +import { CategoryService } from './category.service'; +import type { + CreateCategoryDto, + FindAllCategoriesByProjectIdDto, + FindByCategoryIdDto, + UpdateCategoryDto, +} from './dtos'; +import { + CategoryNameDuplicatedException, + CategoryNameInvalidException, + CategoryNotFoundException, +} from './exceptions'; + +describe('CategoryService', () => { + let service: CategoryService; + let repository: jest.Mocked>; + + const mockCategory = { + id: faker.number.int({ min: 1, max: 1000 }), + name: faker.word.words(2), + project: { + id: faker.number.int({ min: 1, max: 1000 }), + }, + issues: [], + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + deletedAt: null, + beforeInsertHook: jest.fn(), + beforeUpdateHook: jest.fn(), + } as unknown as CategoryEntity; + + beforeEach(async () => { + const mockQueryBuilder = { + leftJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn(), + getCount: jest.fn(), + }; + + const module = await Test.createTestingModule({ + imports: [ClsModule], + providers: [ + CategoryService, + { + provide: getRepositoryToken(CategoryEntity), + useValue: { + createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder), + findOneBy: jest.fn(), + findOne: jest.fn(), + save: jest.fn(), + delete: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(CategoryService); + repository = module.get(getRepositoryToken(CategoryEntity)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should create a category successfully', async () => { + const createDto: CreateCategoryDto = { + projectId: faker.number.int({ min: 1, max: 1000 }), + name: faker.word.words(2), + }; + + repository.findOneBy.mockResolvedValue(null); + repository.save.mockResolvedValue(mockCategory); + + const result = await service.create(createDto); + + expect(repository.findOneBy).toHaveBeenCalledWith({ + name: createDto.name, + project: { id: createDto.projectId }, + }); + expect(repository.save).toHaveBeenCalled(); + expect(result).toEqual(mockCategory); + }); + + it('should throw CategoryNameDuplicatedException when category name already exists', async () => { + const createDto: CreateCategoryDto = { + projectId: faker.number.int({ min: 1, max: 1000 }), + name: faker.word.words(2), + }; + + repository.findOneBy.mockResolvedValue(mockCategory); + + await expect(service.create(createDto)).rejects.toThrow( + CategoryNameDuplicatedException, + ); + expect(repository.save).not.toHaveBeenCalled(); + }); + }); + + describe('findAllByProjectId', () => { + it('should find all categories by project ID', async () => { + const findAllDto: FindAllCategoriesByProjectIdDto = { + projectId: faker.number.int({ min: 1, max: 1000 }), + page: 1, + limit: 10, + }; + + const mockCategories = [mockCategory]; + const totalCount = 1; + + const mockQueryBuilder = + repository.createQueryBuilder as unknown as jest.MockedFunction< + () => { + leftJoin: jest.Mock; + where: jest.Mock; + andWhere: jest.Mock; + addOrderBy: jest.Mock; + offset: jest.Mock; + limit: jest.Mock; + getMany: jest.Mock; + getCount: jest.Mock; + } + >; + mockQueryBuilder.mockReturnValue({ + leftJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(mockCategories), + getCount: jest.fn().mockResolvedValue(totalCount), + }); + + const result = await service.findAllByProjectId(findAllDto); + + expect(repository.createQueryBuilder).toHaveBeenCalledWith('category'); + expect(result.items).toEqual(mockCategories); + expect(result.meta.totalItems).toBe(totalCount); + }); + + it('should filter by category name', async () => { + const findAllDto: FindAllCategoriesByProjectIdDto = { + projectId: faker.number.int({ min: 1, max: 1000 }), + categoryName: 'test', + page: 1, + limit: 10, + }; + + const mockCategories = [mockCategory]; + const totalCount = 1; + + const mockQueryBuilder = + repository.createQueryBuilder as unknown as jest.MockedFunction< + () => { + leftJoin: jest.Mock; + where: jest.Mock; + andWhere: jest.Mock; + addOrderBy: jest.Mock; + offset: jest.Mock; + limit: jest.Mock; + getMany: jest.Mock; + getCount: jest.Mock; + } + >; + mockQueryBuilder.mockReturnValue({ + leftJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(mockCategories), + getCount: jest.fn().mockResolvedValue(totalCount), + }); + + const result = await service.findAllByProjectId(findAllDto); + + expect(result.items).toEqual(mockCategories); + }); + + it('should sort by name', async () => { + const findAllDto: FindAllCategoriesByProjectIdDto = { + projectId: faker.number.int({ min: 1, max: 1000 }), + page: 1, + limit: 10, + sort: { name: SortMethodEnum.ASC }, + }; + + const mockCategories = [mockCategory]; + const totalCount = 1; + + const mockQueryBuilder = + repository.createQueryBuilder as unknown as jest.MockedFunction< + () => { + leftJoin: jest.Mock; + where: jest.Mock; + andWhere: jest.Mock; + addOrderBy: jest.Mock; + offset: jest.Mock; + limit: jest.Mock; + getMany: jest.Mock; + getCount: jest.Mock; + } + >; + mockQueryBuilder.mockReturnValue({ + leftJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(mockCategories), + getCount: jest.fn().mockResolvedValue(totalCount), + }); + + const result = await service.findAllByProjectId(findAllDto); + + expect(result.items).toEqual(mockCategories); + }); + + it('should throw BadRequestException for invalid sort method', async () => { + const findAllDto: FindAllCategoriesByProjectIdDto = { + projectId: faker.number.int({ min: 1, max: 1000 }), + page: 1, + limit: 10, + sort: { name: 'INVALID' as any }, + }; + + await expect(service.findAllByProjectId(findAllDto)).rejects.toThrow( + BadRequestException, + ); + }); + }); + + describe('findById', () => { + it('should find category by ID', async () => { + const findDto: FindByCategoryIdDto = { + categoryId: faker.number.int({ min: 1, max: 1000 }), + }; + + repository.findOne.mockResolvedValue(mockCategory); + + const result = await service.findById(findDto); + + expect(repository.findOne).toHaveBeenCalledWith({ + where: { id: findDto.categoryId }, + relations: { project: true }, + }); + expect(result).toEqual(mockCategory); + }); + + it('should throw CategoryNotFoundException when category not found', async () => { + const findDto: FindByCategoryIdDto = { + categoryId: faker.number.int({ min: 1, max: 1000 }), + }; + + repository.findOne.mockResolvedValue(null); + + await expect(service.findById(findDto)).rejects.toThrow( + CategoryNotFoundException, + ); + }); + }); + + describe('update', () => { + it('should update category successfully', async () => { + const updateDto: UpdateCategoryDto = { + categoryId: faker.number.int({ min: 1, max: 1000 }), + name: faker.word.words(2), + projectId: faker.number.int({ min: 1, max: 1000 }), + }; + + const existingCategory = { + ...mockCategory, + project: { id: updateDto.projectId }, + beforeInsertHook: jest.fn(), + beforeUpdateHook: jest.fn(), + } as unknown as CategoryEntity; + repository.findOne.mockResolvedValueOnce(existingCategory); // findById call + repository.findOne.mockResolvedValueOnce(null); // duplicate check + repository.save.mockResolvedValue({ + ...existingCategory, + ...updateDto, + beforeInsertHook: jest.fn(), + beforeUpdateHook: jest.fn(), + } as unknown as CategoryEntity); + + const result = await service.update(updateDto); + + expect(repository.findOne).toHaveBeenCalledTimes(2); + expect(repository.save).toHaveBeenCalledWith( + Object.assign(existingCategory, updateDto), + ); + expect(result).toEqual({ + ...existingCategory, + ...updateDto, + beforeInsertHook: expect.any(Function), + beforeUpdateHook: expect.any(Function), + }); + }); + + it('should throw CategoryNameInvalidException when category name already exists', async () => { + const updateDto: UpdateCategoryDto = { + categoryId: faker.number.int({ min: 1, max: 1000 }), + name: faker.word.words(2), + projectId: faker.number.int({ min: 1, max: 1000 }), + }; + + const existingCategory = { + ...mockCategory, + project: { id: updateDto.projectId }, + beforeInsertHook: jest.fn(), + beforeUpdateHook: jest.fn(), + } as unknown as CategoryEntity; + repository.findOne.mockResolvedValueOnce(existingCategory); // findById call + repository.findOne.mockResolvedValueOnce(mockCategory); // duplicate check + + await expect(service.update(updateDto)).rejects.toThrow( + CategoryNameInvalidException, + ); + expect(repository.save).not.toHaveBeenCalled(); + }); + }); + + describe('delete', () => { + it('should delete category successfully', async () => { + const deleteParams = { + projectId: faker.number.int({ min: 1, max: 1000 }), + categoryId: faker.number.int({ min: 1, max: 1000 }), + }; + + repository.delete.mockResolvedValue({ affected: 1 } as any); + + await service.delete(deleteParams); + + expect(repository.delete).toHaveBeenCalledWith({ + id: deleteParams.categoryId, + project: { id: deleteParams.projectId }, + }); + }); + + it('should throw CategoryNotFoundException when category to delete not found', async () => { + const deleteParams = { + projectId: faker.number.int({ min: 1, max: 1000 }), + categoryId: faker.number.int({ min: 1, max: 1000 }), + }; + + repository.delete.mockResolvedValue({ affected: 0 } as any); + + await expect(service.delete(deleteParams)).rejects.toThrow( + CategoryNotFoundException, + ); + }); + }); +}); diff --git a/apps/api/src/domains/admin/project/category/exceptions/category-not-found.exception.ts b/apps/api/src/domains/admin/project/category/exceptions/category-not-found.exception.ts index a793bf9b7..de4c48c51 100644 --- a/apps/api/src/domains/admin/project/category/exceptions/category-not-found.exception.ts +++ b/apps/api/src/domains/admin/project/category/exceptions/category-not-found.exception.ts @@ -13,11 +13,11 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { BadRequestException } from '@nestjs/common'; +import { NotFoundException } from '@nestjs/common'; import { ErrorCode } from '@ufb/shared'; -export class CategoryNotFoundException extends BadRequestException { +export class CategoryNotFoundException extends NotFoundException { constructor() { super({ code: ErrorCode.Category.CategoryNotFound, diff --git a/apps/api/src/domains/admin/project/issue/issue.controller.spec.ts b/apps/api/src/domains/admin/project/issue/issue.controller.spec.ts index 0caa284f6..93181b0ec 100644 --- a/apps/api/src/domains/admin/project/issue/issue.controller.spec.ts +++ b/apps/api/src/domains/admin/project/issue/issue.controller.spec.ts @@ -33,6 +33,8 @@ const MockIssueService = { findIssuesByProjectId: jest.fn(), findIssuesByProjectIdV2: jest.fn(), update: jest.fn(), + updateByCategoryId: jest.fn(), + deleteByCategoryId: jest.fn(), deleteById: jest.fn(), deleteByIds: jest.fn(), }; @@ -53,79 +55,150 @@ describe('IssueController', () => { }); describe('create', () => { - it('should return a saved id', async () => { - jest.spyOn(MockIssueService, 'create'); + it('should create an issue and return transformed response', async () => { + const mockIssue = new IssueEntity(); + mockIssue.id = faker.number.int(); + jest.spyOn(MockIssueService, 'create').mockResolvedValue(mockIssue); const projectId = faker.number.int(); const dto = new CreateIssueRequestDto(); dto.name = faker.string.sample(); - await issueController.create(projectId, dto); + const result = await issueController.create(projectId, dto); expect(MockIssueService.create).toHaveBeenCalledTimes(1); + expect(MockIssueService.create).toHaveBeenCalledWith( + expect.objectContaining({ + projectId, + name: dto.name, + }), + ); + expect(result).toBeDefined(); }); }); describe('findById', () => { - it('should return an issue', async () => { + it('should return a transformed issue', async () => { const issueId = faker.number.int(); const issue = new IssueEntity(); - jest.spyOn(MockIssueService, 'findById').mockReturnValue(issue); + issue.id = issueId; + issue.name = faker.string.sample(); + jest.spyOn(MockIssueService, 'findById').mockResolvedValue(issue); - await issueController.findById(issueId); + const result = await issueController.findById(issueId); expect(MockIssueService.findById).toHaveBeenCalledTimes(1); + expect(MockIssueService.findById).toHaveBeenCalledWith({ issueId }); + expect(result).toBeDefined(); }); }); describe('findAllByProjectId', () => { - it('should return issues', async () => { + it('should return transformed issues by project id', async () => { const projectId = faker.number.int(); - const issues = [new IssueEntity()]; + const mockResult = { + issues: [new IssueEntity()], + total: 1, + page: 1, + limit: 10, + }; jest - .spyOn(MockIssueService, 'findIssuesByProjectId') - .mockReturnValue(issues); + .spyOn(MockIssueService, 'findIssuesByProjectIdV2') + .mockResolvedValue(mockResult); - await issueController.findAllByProjectId(projectId, { + const searchDto = { page: 1, limit: 10, - }); + }; + + const result = await issueController.findAllByProjectId( + projectId, + searchDto, + ); expect(MockIssueService.findIssuesByProjectIdV2).toHaveBeenCalledTimes(1); + expect(MockIssueService.findIssuesByProjectIdV2).toHaveBeenCalledWith({ + ...searchDto, + projectId, + }); + expect(result).toBeDefined(); }); }); describe('update', () => { - it('', async () => { + it('should update an issue', async () => { const projectId = faker.number.int(); const issueId = faker.number.int(); const dto = new UpdateIssueRequestDto(); dto.name = faker.string.sample(); dto.description = faker.string.sample(); - jest.spyOn(MockIssueService, 'update'); + jest.spyOn(MockIssueService, 'update').mockResolvedValue(undefined); await issueController.update(projectId, issueId, dto); expect(MockIssueService.update).toHaveBeenCalledTimes(1); + expect(MockIssueService.update).toHaveBeenCalledWith({ + ...dto, + issueId, + projectId, + }); }); }); describe('delete', () => { - it('', async () => { + it('should delete an issue by id', async () => { const issueId = faker.number.int(); - jest.spyOn(MockIssueService, 'deleteById'); + jest.spyOn(MockIssueService, 'deleteById').mockResolvedValue(undefined); await issueController.delete(issueId); expect(MockIssueService.deleteById).toHaveBeenCalledTimes(1); + expect(MockIssueService.deleteById).toHaveBeenCalledWith(issueId); }); }); describe('deleteMany', () => { - it('', async () => { + it('should delete multiple issues by ids', async () => { const projectId = faker.number.int(); const dto = new DeleteIssuesRequestDto(); - dto.issueIds = [faker.number.int()]; - jest.spyOn(MockIssueService, 'deleteByIds'); + dto.issueIds = [faker.number.int(), faker.number.int()]; + jest.spyOn(MockIssueService, 'deleteByIds').mockResolvedValue(undefined); await issueController.deleteMany(projectId, dto); expect(MockIssueService.deleteByIds).toHaveBeenCalledTimes(1); + expect(MockIssueService.deleteByIds).toHaveBeenCalledWith(dto.issueIds); + }); + }); + + describe('updateByCategoryId', () => { + it('should update issue category', async () => { + const issueId = faker.number.int(); + const categoryId = faker.number.int(); + jest + .spyOn(MockIssueService, 'updateByCategoryId') + .mockResolvedValue(undefined); + + await issueController.updateByCategoryId(issueId, categoryId); + + expect(MockIssueService.updateByCategoryId).toHaveBeenCalledTimes(1); + expect(MockIssueService.updateByCategoryId).toHaveBeenCalledWith({ + issueId, + categoryId, + }); + }); + }); + + describe('deleteByCategoryId', () => { + it('should delete issue category', async () => { + const issueId = faker.number.int(); + const categoryId = faker.number.int(); + jest + .spyOn(MockIssueService, 'deleteByCategoryId') + .mockResolvedValue(undefined); + + await issueController.deleteByCategoryId(issueId, categoryId); + + expect(MockIssueService.deleteByCategoryId).toHaveBeenCalledTimes(1); + expect(MockIssueService.deleteByCategoryId).toHaveBeenCalledWith({ + issueId, + categoryId, + }); }); }); }); diff --git a/apps/api/src/domains/admin/project/issue/issue.service.spec.ts b/apps/api/src/domains/admin/project/issue/issue.service.spec.ts index 9106ee6df..77d71eeb1 100644 --- a/apps/api/src/domains/admin/project/issue/issue.service.spec.ts +++ b/apps/api/src/domains/admin/project/issue/issue.service.spec.ts @@ -14,25 +14,40 @@ * under the License. */ import { faker } from '@faker-js/faker'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import type { Repository, SelectQueryBuilder } from 'typeorm'; -import { Like } from 'typeorm'; +import { In, Like } from 'typeorm'; import type { TimeRange } from '@/common/dtos'; -import { IssueStatusEnum } from '@/common/enums'; +import type { SortMethodEnum } from '@/common/enums'; +import { + EventTypeEnum, + IssueStatusEnum, + QueryV2ConditionsEnum, +} from '@/common/enums'; import { IssueStatisticsEntity } from '@/domains/admin/statistics/issue/issue-statistics.entity'; +import { IssueStatisticsService } from '@/domains/admin/statistics/issue/issue-statistics.service'; +import { SchedulerLockService } from '@/domains/operation/scheduler-lock/scheduler-lock.service'; import { issueFixture } from '@/test-utils/fixtures'; import { createQueryBuilder, TestConfig } from '@/test-utils/util-functions'; import { IssueServiceProviders } from '../../../../test-utils/providers/issue.service.providers'; +import { CategoryEntity } from '../category/category.entity'; +import { CategoryNotFoundException } from '../category/exceptions'; +import { ProjectEntity } from '../project/project.entity'; import { CreateIssueDto, FindIssuesByProjectIdDto, + FindIssuesByProjectIdDtoV2, UpdateIssueDto, } from './dtos'; +import { UpdateIssueCategoryDto } from './dtos/update-issue-category.dto'; import { IssueInvalidNameException, IssueNameDuplicatedException, + IssueNotFoundException, } from './exceptions'; import { IssueEntity } from './issue.entity'; import { IssueService } from './issue.service'; @@ -41,16 +56,57 @@ describe('IssueService test suite', () => { let issueService: IssueService; let issueRepo: Repository; let issueStatsRepo: Repository; + let categoryRepo: Repository; + let _projectRepo: Repository; + let eventEmitter: EventEmitter2; + let _schedulerRegistry: SchedulerRegistry; + let _schedulerLockService: SchedulerLockService; + let issueStatisticsService: IssueStatisticsService; beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestConfig], - providers: IssueServiceProviders, + providers: [ + ...IssueServiceProviders, + { + provide: getRepositoryToken(ProjectEntity), + useValue: { + find: jest.fn(), + findOne: jest.fn(), + findOneBy: jest.fn(), + save: jest.fn(), + remove: jest.fn(), + count: jest.fn(), + createQueryBuilder: jest.fn(), + }, + }, + { + provide: getRepositoryToken(CategoryEntity), + useValue: { + find: jest.fn(), + findOne: jest.fn(), + findOneBy: jest.fn(), + save: jest.fn(), + remove: jest.fn(), + count: jest.fn(), + createQueryBuilder: jest.fn(), + }, + }, + ], }).compile(); issueService = module.get(IssueService); issueRepo = module.get(getRepositoryToken(IssueEntity)); issueStatsRepo = module.get(getRepositoryToken(IssueStatisticsEntity)); + categoryRepo = module.get(getRepositoryToken(CategoryEntity)); + _projectRepo = module.get(getRepositoryToken(ProjectEntity)); + eventEmitter = module.get(EventEmitter2); + _schedulerRegistry = module.get(SchedulerRegistry); + _schedulerLockService = + module.get(SchedulerLockService); + issueStatisticsService = module.get( + IssueStatisticsService, + ); }); describe('create', () => { @@ -64,12 +120,25 @@ describe('IssueService test suite', () => { it('creating an issue succeeds with valid inputs', async () => { dto.name = faker.string.sample(); jest.spyOn(issueRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(issueRepo, 'save').mockResolvedValue({ + ...issueFixture, + name: dto.name, + project: { id: projectId }, + status: IssueStatusEnum.INIT, + feedbackCount: 0, + } as IssueEntity); jest .spyOn(issueStatsRepo, 'createQueryBuilder') .mockImplementation( () => createQueryBuilder as unknown as SelectQueryBuilder, ); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockResolvedValue(undefined); + const eventEmitterSpy = jest + .spyOn(eventEmitter, 'emit') + .mockImplementation(); const issue = await issueService.create(dto); @@ -77,6 +146,12 @@ describe('IssueService test suite', () => { expect(issue.project.id).toBe(projectId); expect(issue.status).toBe(IssueStatusEnum.INIT); expect(issue.feedbackCount).toBe(0); + expect(eventEmitterSpy).toHaveBeenCalledWith( + EventTypeEnum.ISSUE_CREATION, + { + issueId: issue.id, + }, + ); }); it('creating an issue fails with a duplicate name', async () => { const duplicateName = 'duplicateName'; @@ -315,19 +390,723 @@ describe('IssueService test suite', () => { it('updating an issue succeeds with valid inputs', async () => { dto.name = faker.string.sample(); + const existingIssue = { + ...issueFixture, + id: issueId, + project: { id: faker.number.int() }, + }; + jest + .spyOn(issueService, 'findById') + .mockResolvedValue(existingIssue as IssueEntity); jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(issueRepo, 'save').mockResolvedValue({ + ...existingIssue, + name: dto.name, + description: dto.description, + } as IssueEntity); + jest.spyOn(eventEmitter, 'emit').mockImplementation(); const issue = await issueService.update(dto); expect(issue.name).toBe(dto.name); + expect(issue.description).toBe(dto.description); + }); + + it('updating an issue emits status change event when status changes', async () => { + dto.name = faker.string.sample(); + dto.status = IssueStatusEnum.IN_PROGRESS; + const existingIssue = { + ...issueFixture, + id: issueId, + status: IssueStatusEnum.INIT, + project: { id: faker.number.int() }, + }; + jest + .spyOn(issueService, 'findById') + .mockResolvedValue(existingIssue as IssueEntity); + jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(issueRepo, 'save').mockResolvedValue({ + ...existingIssue, + name: dto.name, + status: dto.status, + } as IssueEntity); + const eventEmitterSpy = jest + .spyOn(eventEmitter, 'emit') + .mockImplementation(); + + await issueService.update(dto); + + expect(eventEmitterSpy).toHaveBeenCalledWith( + EventTypeEnum.ISSUE_STATUS_CHANGE, + { + issueId, + previousStatus: IssueStatusEnum.INIT, + }, + ); }); it('updating an issue fails with a duplicate name', async () => { const duplicateName = issueFixture.name; dto.name = duplicateName; + const existingIssue = { + ...issueFixture, + id: issueId, + project: { id: faker.number.int() }, + }; + jest + .spyOn(issueService, 'findById') + .mockResolvedValue(existingIssue as IssueEntity); + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue({ id: faker.number.int() } as IssueEntity); await expect(issueService.update(dto)).rejects.toThrow( new IssueInvalidNameException('Duplicated name'), ); }); }); + + describe('findIssuesByProjectIdV2', () => { + const projectId = faker.number.int(); + let dto: FindIssuesByProjectIdDtoV2; + + beforeEach(() => { + dto = new FindIssuesByProjectIdDtoV2(); + dto.projectId = projectId; + dto.page = 1; + dto.limit = 10; + }); + + it('finding issues with V2 API succeeds with basic query', async () => { + dto.queries = [ + { + key: 'name', + value: 'test', + condition: QueryV2ConditionsEnum.CONTAINS, + }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([issueFixture]), + getCount: jest.fn().mockResolvedValue(1), + }; + + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await issueService.findIssuesByProjectIdV2(dto); + + expect(result.meta.itemCount).toBe(1); + expect(result.meta.totalItems).toBe(1); + expect(result.meta.currentPage).toBe(1); + }); + + it('finding issues with V2 API succeeds with category query', async () => { + dto.queries = [ + { key: 'categoryId', value: 1, condition: QueryV2ConditionsEnum.IS }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([issueFixture]), + getCount: jest.fn().mockResolvedValue(1), + }; + + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await issueService.findIssuesByProjectIdV2(dto); + + expect(result.meta.itemCount).toBe(1); + }); + + it('finding issues with V2 API succeeds with null category query', async () => { + dto.queries = [ + { key: 'categoryId', value: 0, condition: QueryV2ConditionsEnum.IS }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([issueFixture]), + getCount: jest.fn().mockResolvedValue(1), + }; + + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await issueService.findIssuesByProjectIdV2(dto); + + expect(result.meta.itemCount).toBe(1); + }); + + it('finding issues with V2 API succeeds with OR operator', async () => { + dto.operator = 'OR'; + dto.queries = [ + { + key: 'name', + value: 'test1', + condition: QueryV2ConditionsEnum.CONTAINS, + }, + { + key: 'name', + value: 'test2', + condition: QueryV2ConditionsEnum.CONTAINS, + }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([issueFixture]), + getCount: jest.fn().mockResolvedValue(1), + }; + + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await issueService.findIssuesByProjectIdV2(dto); + + expect(result.meta.itemCount).toBe(1); + }); + + it('finding issues with V2 API throws error for invalid sort method', async () => { + dto.sort = { name: 'INVALID_SORT' as SortMethodEnum }; + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockImplementation( + () => + createQueryBuilder as unknown as SelectQueryBuilder, + ); + + await expect(issueService.findIssuesByProjectIdV2(dto)).rejects.toThrow(); + }); + }); + + describe('findById', () => { + const issueId = faker.number.int(); + + it('finding issue by id succeeds when issue exists', async () => { + const mockIssue = { ...issueFixture, id: issueId }; + jest + .spyOn(issueRepo, 'find') + .mockResolvedValue([mockIssue] as IssueEntity[]); + + const result = await issueService.findById({ issueId }); + + expect(result).toEqual(mockIssue); + expect(issueRepo.find).toHaveBeenCalledWith({ + where: { id: issueId }, + relations: { project: true }, + }); + }); + + it('finding issue by id throws exception when issue not found', async () => { + jest.spyOn(issueRepo, 'find').mockResolvedValue([]); + + await expect(issueService.findById({ issueId })).rejects.toThrow( + IssueNotFoundException, + ); + }); + }); + + describe('findByName', () => { + const issueName = faker.string.sample(); + + it('finding issue by name succeeds when issue exists', async () => { + const mockIssue = { ...issueFixture, name: issueName }; + jest + .spyOn(issueRepo, 'findOneBy') + .mockResolvedValue(mockIssue as IssueEntity); + + const result = await issueService.findByName({ name: issueName }); + + expect(result).toEqual(mockIssue); + expect(issueRepo.findOneBy).toHaveBeenCalledWith({ name: issueName }); + }); + + it('finding issue by name returns null when issue not found', async () => { + jest.spyOn(issueRepo, 'findOneBy').mockResolvedValue(null); + + const result = await issueService.findByName({ name: issueName }); + + expect(result).toBeNull(); + }); + }); + + describe('findIssuesByFeedbackIds', () => { + const feedbackIds = [faker.number.int(), faker.number.int()]; + + it('finding issues by feedback ids succeeds', async () => { + const mockIssues = [ + { ...issueFixture, id: 1, feedbacks: [{ id: feedbackIds[0] }] }, + { ...issueFixture, id: 2, feedbacks: [{ id: feedbackIds[1] }] }, + ]; + jest + .spyOn(issueRepo, 'find') + .mockResolvedValue(mockIssues as IssueEntity[]); + + const result = await issueService.findIssuesByFeedbackIds(feedbackIds); + + expect(result[feedbackIds[0]]).toHaveLength(1); + expect(result[feedbackIds[1]]).toHaveLength(1); + expect(issueRepo.find).toHaveBeenCalledWith({ + relations: { feedbacks: true }, + where: { feedbacks: { id: In(feedbackIds) } }, + order: { id: 'ASC' }, + }); + }); + + it('finding issues by feedback ids returns empty arrays for non-existent feedbacks', async () => { + jest.spyOn(issueRepo, 'find').mockResolvedValue([]); + + const result = await issueService.findIssuesByFeedbackIds(feedbackIds); + + expect(result[feedbackIds[0]]).toEqual([]); + expect(result[feedbackIds[1]]).toEqual([]); + }); + }); + + describe('updateByCategoryId', () => { + const issueId = faker.number.int(); + const categoryId = faker.number.int(); + let dto: UpdateIssueCategoryDto; + + beforeEach(() => { + dto = new UpdateIssueCategoryDto(); + dto.issueId = issueId; + dto.categoryId = categoryId; + }); + + it('updating issue category succeeds with valid inputs', async () => { + const mockIssue = { ...issueFixture, id: issueId }; + const mockCategory = { id: categoryId }; + + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + jest + .spyOn(categoryRepo, 'findOne') + .mockResolvedValue(mockCategory as CategoryEntity); + jest.spyOn(issueRepo, 'save').mockResolvedValue({ + ...mockIssue, + category: { id: categoryId }, + } as IssueEntity); + + const result = await issueService.updateByCategoryId(dto); + + expect(result.category?.id).toBe(categoryId); + }); + + it('updating issue category throws exception when issue not found', async () => { + jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + + await expect(issueService.updateByCategoryId(dto)).rejects.toThrow( + IssueNotFoundException, + ); + }); + + it('updating issue category throws exception when category not found', async () => { + const mockIssue = { ...issueFixture, id: issueId }; + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + jest.spyOn(categoryRepo, 'findOne').mockResolvedValue(null); + + await expect(issueService.updateByCategoryId(dto)).rejects.toThrow( + CategoryNotFoundException, + ); + }); + }); + + describe('deleteByCategoryId', () => { + const issueId = faker.number.int(); + const categoryId = faker.number.int(); + + it('deleting issue category succeeds with valid inputs', async () => { + const mockIssue = { + ...issueFixture, + id: issueId, + category: { id: categoryId }, + }; + + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + jest.spyOn(issueRepo, 'save').mockResolvedValue({ + ...mockIssue, + category: null, + } as IssueEntity); + + const result = await issueService.deleteByCategoryId({ + issueId, + categoryId, + }); + + expect(result.category).toBeNull(); + }); + + it('deleting issue category throws exception when issue not found', async () => { + jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + + await expect( + issueService.deleteByCategoryId({ issueId, categoryId }), + ).rejects.toThrow(IssueNotFoundException); + }); + + it('deleting issue category throws exception when category id does not match', async () => { + const mockIssue = { + ...issueFixture, + id: issueId, + category: { id: faker.number.int() }, + }; + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + + await expect( + issueService.deleteByCategoryId({ issueId, categoryId }), + ).rejects.toThrow(); + }); + }); + + describe('deleteById', () => { + const issueId = faker.number.int(); + + it('deleting issue by id succeeds', async () => { + const mockIssue = { + ...issueFixture, + id: issueId, + project: { id: faker.number.int() }, + }; + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockResolvedValue(undefined); + jest + .spyOn(issueRepo, 'remove') + .mockResolvedValue(mockIssue as IssueEntity); + + await issueService.deleteById(issueId); + + expect(issueStatisticsService.updateCount).toHaveBeenCalledWith({ + projectId: mockIssue.project.id, + date: mockIssue.createdAt, + count: -1, + }); + expect(issueRepo.remove).toHaveBeenCalledWith(mockIssue); + }); + + it('deleting issue by id throws error when issue not found due to missing project info', async () => { + jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + + // IssueService에서 new IssueEntity()를 생성하므로 project가 undefined가 되어 오류 발생 + await expect(issueService.deleteById(issueId)).rejects.toThrow(); + }); + }); + + describe('deleteByIds', () => { + const issueIds = [faker.number.int(), faker.number.int()]; + + it('deleting issues by ids succeeds', async () => { + const mockIssues = [ + { + ...issueFixture, + id: issueIds[0], + project: { id: faker.number.int() }, + }, + { + ...issueFixture, + id: issueIds[1], + project: { id: faker.number.int() }, + }, + ]; + jest + .spyOn(issueRepo, 'find') + .mockResolvedValue(mockIssues as IssueEntity[]); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockResolvedValue(undefined); + jest + .spyOn(issueRepo, 'remove') + .mockResolvedValue(mockIssues[0] as IssueEntity); + + await issueService.deleteByIds(issueIds); + + expect(issueStatisticsService.updateCount).toHaveBeenCalledTimes(2); + expect(issueRepo.remove).toHaveBeenCalledWith(mockIssues); + }); + + it('deleting issues by ids succeeds with empty array', async () => { + jest.spyOn(issueRepo, 'find').mockResolvedValue([]); + jest.spyOn(issueRepo, 'remove').mockResolvedValue([] as any); + + await issueService.deleteByIds(issueIds); + + expect(issueRepo.remove).toHaveBeenCalledWith([]); + }); + }); + + describe('countByProjectId', () => { + const projectId = faker.number.int(); + + it('counting issues by project id succeeds', async () => { + const mockCount = faker.number.int(); + jest.spyOn(issueRepo, 'count').mockResolvedValue(mockCount); + + const result = await issueService.countByProjectId({ projectId }); + + expect(result.total).toBe(mockCount); + expect(issueRepo.count).toHaveBeenCalledWith({ + relations: { project: true }, + where: { project: { id: projectId } }, + }); + }); + }); + + describe('calculateFeedbackCount', () => { + const projectId = faker.number.int(); + + it('calculating feedback count succeeds', async () => { + const mockQueryBuilder = { + update: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + execute: jest.fn().mockResolvedValue({}), + }; + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + await issueService.calculateFeedbackCount(projectId); + + expect(mockQueryBuilder.update).toHaveBeenCalledWith('issues'); + expect(mockQueryBuilder.where).toHaveBeenCalledWith( + 'project_id = :projectId', + { projectId }, + ); + expect(mockQueryBuilder.execute).toHaveBeenCalled(); + }); + }); + + describe('Transactional behavior', () => { + describe('create method', () => { + const projectId = faker.number.int(); + let dto: CreateIssueDto; + + beforeEach(() => { + dto = new CreateIssueDto(); + dto.projectId = projectId; + dto.name = faker.string.sample(); + }); + + it('create method handles transaction rollback when statistics update fails', async () => { + jest.spyOn(issueRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(issueRepo, 'save').mockResolvedValue({ + ...issueFixture, + name: dto.name, + project: { id: projectId }, + } as IssueEntity); + jest + .spyOn(issueStatsRepo, 'createQueryBuilder') + .mockImplementation( + () => + createQueryBuilder as unknown as SelectQueryBuilder, + ); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockRejectedValue(new Error('Statistics update failed')); + + await expect(issueService.create(dto)).rejects.toThrow( + 'Statistics update failed', + ); + }); + }); + + describe('update method', () => { + const issueId = faker.number.int(); + let dto: UpdateIssueDto; + + beforeEach(() => { + dto = new UpdateIssueDto(); + dto.issueId = issueId; + dto.name = faker.string.sample(); + dto.description = faker.string.sample(); + }); + + it('update method handles transaction rollback when save fails', async () => { + const existingIssue = { + ...issueFixture, + id: issueId, + project: { id: faker.number.int() }, + }; + jest + .spyOn(issueService, 'findById') + .mockResolvedValue(existingIssue as IssueEntity); + jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + jest + .spyOn(issueRepo, 'save') + .mockRejectedValue(new Error('Database save failed')); + + await expect(issueService.update(dto)).rejects.toThrow( + 'Database save failed', + ); + }); + }); + + describe('updateByCategoryId method', () => { + const issueId = faker.number.int(); + const categoryId = faker.number.int(); + let dto: UpdateIssueCategoryDto; + + beforeEach(() => { + dto = new UpdateIssueCategoryDto(); + dto.issueId = issueId; + dto.categoryId = categoryId; + }); + + it('updateByCategoryId method handles transaction rollback when save fails', async () => { + const mockIssue = { ...issueFixture, id: issueId }; + const mockCategory = { id: categoryId }; + + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + jest + .spyOn(categoryRepo, 'findOne') + .mockResolvedValue(mockCategory as CategoryEntity); + jest + .spyOn(issueRepo, 'save') + .mockRejectedValue(new Error('Database save failed')); + + await expect(issueService.updateByCategoryId(dto)).rejects.toThrow( + 'Database save failed', + ); + }); + }); + + describe('deleteById method', () => { + const issueId = faker.number.int(); + + it('deleteById method handles transaction rollback when statistics update fails', async () => { + const mockIssue = { + ...issueFixture, + id: issueId, + project: { id: faker.number.int() }, + }; + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockRejectedValue(new Error('Statistics update failed')); + + await expect(issueService.deleteById(issueId)).rejects.toThrow( + 'Statistics update failed', + ); + }); + + it('deleteById method handles transaction rollback when remove fails', async () => { + const mockIssue = { + ...issueFixture, + id: issueId, + project: { id: faker.number.int() }, + }; + jest + .spyOn(issueRepo, 'findOne') + .mockResolvedValue(mockIssue as IssueEntity); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockResolvedValue(undefined); + jest + .spyOn(issueRepo, 'remove') + .mockRejectedValue(new Error('Database remove failed')); + + await expect(issueService.deleteById(issueId)).rejects.toThrow( + 'Database remove failed', + ); + }); + }); + + describe('deleteByIds method', () => { + const issueIds = [faker.number.int(), faker.number.int()]; + + it('deleteByIds method handles transaction rollback when statistics update fails', async () => { + const mockIssues = [ + { + ...issueFixture, + id: issueIds[0], + project: { id: faker.number.int() }, + }, + { + ...issueFixture, + id: issueIds[1], + project: { id: faker.number.int() }, + }, + ]; + jest + .spyOn(issueRepo, 'find') + .mockResolvedValue(mockIssues as IssueEntity[]); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockRejectedValue(new Error('Statistics update failed')); + + await expect(issueService.deleteByIds(issueIds)).rejects.toThrow( + 'Statistics update failed', + ); + }); + + it('deleteByIds method handles transaction rollback when remove fails', async () => { + const mockIssues = [ + { + ...issueFixture, + id: issueIds[0], + project: { id: faker.number.int() }, + }, + { + ...issueFixture, + id: issueIds[1], + project: { id: faker.number.int() }, + }, + ]; + jest + .spyOn(issueRepo, 'find') + .mockResolvedValue(mockIssues as IssueEntity[]); + jest + .spyOn(issueStatisticsService, 'updateCount') + .mockResolvedValue(undefined); + jest + .spyOn(issueRepo, 'remove') + .mockRejectedValue(new Error('Database remove failed')); + + await expect(issueService.deleteByIds(issueIds)).rejects.toThrow( + 'Database remove failed', + ); + }); + }); + }); }); diff --git a/apps/api/src/domains/admin/project/issue/issue.service.ts b/apps/api/src/domains/admin/project/issue/issue.service.ts index 8235a0dc9..73d155cf3 100644 --- a/apps/api/src/domains/admin/project/issue/issue.service.ts +++ b/apps/api/src/domains/admin/project/issue/issue.service.ts @@ -406,7 +406,7 @@ export class IssueService { if (!issue) throw new IssueNotFoundException(); - if (!issue.category || issue.category.id !== categoryId) { + if (issue.category?.id !== categoryId) { throw new BadRequestException('Category id does not match'); } diff --git a/apps/api/src/domains/admin/project/member/member.controller.spec.ts b/apps/api/src/domains/admin/project/member/member.controller.spec.ts new file mode 100644 index 000000000..70ed4f04d --- /dev/null +++ b/apps/api/src/domains/admin/project/member/member.controller.spec.ts @@ -0,0 +1,273 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; + +import { getMockProvider, MockDataSource } from '@/test-utils/util-functions'; +import type { GetAllMemberRequestDto } from './dtos/requests'; +import type { CreateMemberRequestDto } from './dtos/requests/create-member-request.dto'; +import type { DeleteManyMemberRequestDto } from './dtos/requests/delete-many-member-request.dto'; +import type { UpdateMemberRequestDto } from './dtos/requests/update-member-request.dto'; +import { MemberController } from './member.controller'; +import { MemberService } from './member.service'; + +const MockMemberService = { + findAll: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + deleteMany: jest.fn(), +}; + +describe('MemberController', () => { + let memberController: MemberController; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + controllers: [MemberController], + providers: [ + getMockProvider(MemberService, MockMemberService), + getMockProvider(DataSource, MockDataSource), + ], + }).compile(); + + memberController = module.get(MemberController); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('searchMembers', () => { + it('should call memberService.findAll with correct parameters', async () => { + const projectId = faker.number.int(); + const requestDto: GetAllMemberRequestDto = { + limit: 10, + page: 1, + queries: [ + { + key: 'name', + value: 'test', + condition: 'LIKE' as any, + }, + ], + operator: 'AND', + }; + + const mockResponse = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 10, + currentPage: 1, + totalPages: 0, + }, + }; + + MockMemberService.findAll.mockResolvedValue(mockResponse); + + const result = await memberController.searchMembers( + projectId, + requestDto, + ); + + expect(MockMemberService.findAll).toHaveBeenCalledWith({ + options: { limit: requestDto.limit, page: requestDto.page }, + queries: requestDto.queries, + operator: requestDto.operator, + projectId, + }); + expect(result).toBeDefined(); + }); + + it('should handle search without queries and operator', async () => { + const projectId = faker.number.int(); + const requestDto: GetAllMemberRequestDto = { + limit: 20, + page: 2, + }; + + const mockResponse = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 20, + currentPage: 2, + totalPages: 0, + }, + }; + + MockMemberService.findAll.mockResolvedValue(mockResponse); + + await memberController.searchMembers(projectId, requestDto); + + expect(MockMemberService.findAll).toHaveBeenCalledWith({ + options: { limit: requestDto.limit, page: requestDto.page }, + queries: undefined, + operator: undefined, + projectId, + }); + }); + }); + + describe('create', () => { + it('should call memberService.create with correct parameters', async () => { + const requestDto: CreateMemberRequestDto = { + userId: faker.number.int(), + roleId: faker.number.int(), + }; + + MockMemberService.create.mockResolvedValue(undefined); + + await memberController.create(requestDto); + + expect(MockMemberService.create).toHaveBeenCalledWith(requestDto); + expect(MockMemberService.create).toHaveBeenCalledTimes(1); + }); + }); + + describe('update', () => { + it('should call memberService.update with correct parameters', async () => { + const memberId = faker.number.int(); + const requestDto: UpdateMemberRequestDto = { + roleId: faker.number.int(), + }; + + MockMemberService.update.mockResolvedValue(undefined); + + await memberController.update(memberId, requestDto); + + expect(MockMemberService.update).toHaveBeenCalledWith({ + ...requestDto, + memberId, + }); + expect(MockMemberService.update).toHaveBeenCalledTimes(1); + }); + }); + + describe('delete', () => { + it('should call memberService.delete with correct memberId', async () => { + const memberId = faker.number.int(); + + MockMemberService.delete.mockResolvedValue(undefined); + + await memberController.delete(memberId); + + expect(MockMemberService.delete).toHaveBeenCalledWith(memberId); + expect(MockMemberService.delete).toHaveBeenCalledTimes(1); + }); + }); + + describe('deleteMany', () => { + it('should call memberService.deleteMany with correct parameters', async () => { + const requestDto: DeleteManyMemberRequestDto = { + memberIds: [faker.number.int(), faker.number.int(), faker.number.int()], + }; + + MockMemberService.deleteMany.mockResolvedValue(undefined); + + await memberController.deleteMany(requestDto); + + expect(MockMemberService.deleteMany).toHaveBeenCalledWith(requestDto); + expect(MockMemberService.deleteMany).toHaveBeenCalledTimes(1); + }); + + it('should handle empty memberIds array', async () => { + const requestDto: DeleteManyMemberRequestDto = { + memberIds: [], + }; + + MockMemberService.deleteMany.mockResolvedValue(undefined); + + await memberController.deleteMany(requestDto); + + expect(MockMemberService.deleteMany).toHaveBeenCalledWith(requestDto); + expect(MockMemberService.deleteMany).toHaveBeenCalledTimes(1); + }); + }); + + describe('error handling', () => { + it('should propagate errors from memberService.findAll', async () => { + const projectId = faker.number.int(); + const requestDto: GetAllMemberRequestDto = { + limit: 10, + page: 1, + }; + + const error = new Error('Service error'); + MockMemberService.findAll.mockRejectedValue(error); + + await expect( + memberController.searchMembers(projectId, requestDto), + ).rejects.toThrow('Service error'); + }); + + it('should propagate errors from memberService.create', async () => { + const requestDto: CreateMemberRequestDto = { + userId: faker.number.int(), + roleId: faker.number.int(), + }; + + const error = new Error('Create error'); + MockMemberService.create.mockRejectedValue(error); + + await expect(memberController.create(requestDto)).rejects.toThrow( + 'Create error', + ); + }); + + it('should propagate errors from memberService.update', async () => { + const memberId = faker.number.int(); + const requestDto: UpdateMemberRequestDto = { + roleId: faker.number.int(), + }; + + const error = new Error('Update error'); + MockMemberService.update.mockRejectedValue(error); + + await expect( + memberController.update(memberId, requestDto), + ).rejects.toThrow('Update error'); + }); + + it('should propagate errors from memberService.delete', async () => { + const memberId = faker.number.int(); + + const error = new Error('Delete error'); + MockMemberService.delete.mockRejectedValue(error); + + await expect(memberController.delete(memberId)).rejects.toThrow( + 'Delete error', + ); + }); + + it('should propagate errors from memberService.deleteMany', async () => { + const requestDto: DeleteManyMemberRequestDto = { + memberIds: [faker.number.int()], + }; + + const error = new Error('Delete many error'); + MockMemberService.deleteMany.mockRejectedValue(error); + + await expect(memberController.deleteMany(requestDto)).rejects.toThrow( + 'Delete many error', + ); + }); + }); +}); diff --git a/apps/api/src/domains/admin/project/member/member.service.spec.ts b/apps/api/src/domains/admin/project/member/member.service.spec.ts index 2608cc098..43b953bab 100644 --- a/apps/api/src/domains/admin/project/member/member.service.spec.ts +++ b/apps/api/src/domains/admin/project/member/member.service.spec.ts @@ -18,22 +18,32 @@ import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import type { Repository } from 'typeorm'; +import { SortMethodEnum } from '@/common/enums'; +import { NotAllowedDomainException } from '@/domains/admin/user/exceptions'; +import { UserService } from '@/domains/admin/user/user.service'; import { TestConfig } from '@/test-utils/util-functions'; import { MemberServiceProviders } from '../../../../test-utils/providers/member.service.providers'; -import { RoleEntity } from '../role/role.entity'; +import { TenantEntity } from '../../tenant/tenant.entity'; +import type { RoleEntity } from '../role/role.entity'; +import { RoleService } from '../role/role.service'; import { CreateMemberDto, UpdateMemberDto } from './dtos'; +import type { FindAllMembersDto } from './dtos'; +import type { DeleteManyMemberRequestDto } from './dtos/requests/delete-many-member-request.dto'; import { MemberAlreadyExistsException, MemberNotFoundException, MemberUpdateRoleNotMatchedProjectException, } from './exceptions'; +import { MemberInvalidUserException } from './exceptions/member-invalid-user.exception'; import { MemberEntity } from './member.entity'; import { MemberService } from './member.service'; describe('MemberService test suite', () => { let memberService: MemberService; let memberRepo: Repository; - let roleRepo: Repository; + let tenantRepo: Repository; + let userService: UserService; + let roleService: RoleService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -43,7 +53,9 @@ describe('MemberService test suite', () => { memberService = module.get(MemberService); memberRepo = module.get(getRepositoryToken(MemberEntity)); - roleRepo = module.get(getRepositoryToken(RoleEntity)); + tenantRepo = module.get(getRepositoryToken(TenantEntity)); + userService = module.get(UserService); + roleService = module.get(RoleService); }); describe('create', () => { @@ -58,26 +70,26 @@ describe('MemberService test suite', () => { }); it('creating a member succeeds with valid inputs', async () => { - jest - .spyOn(roleRepo, 'findOne') - .mockResolvedValue({ project: { id: projectId } } as RoleEntity); + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); + jest.spyOn(userService, 'findById').mockResolvedValue({} as any); jest.spyOn(memberRepo, 'findOne').mockResolvedValue(null); - jest.spyOn(memberRepo, 'save'); + jest.spyOn(memberRepo, 'save').mockResolvedValue({} as MemberEntity); await memberService.create(dto); - expect(roleRepo.findOne).toHaveBeenCalledTimes(1); + expect(roleService.findById).toHaveBeenCalledWith(roleId); + expect(userService.findById).toHaveBeenCalledWith(userId); expect(memberRepo.findOne).toHaveBeenCalledTimes(1); expect(memberRepo.save).toHaveBeenCalledTimes(1); - expect(memberRepo.save).toHaveBeenCalledWith({ - role: { id: roleId }, - user: { id: userId }, - }); }); + it('creating a member fails with an existent member', async () => { - jest - .spyOn(roleRepo, 'findOne') - .mockResolvedValue({ project: { id: projectId } } as RoleEntity); + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); + jest.spyOn(userService, 'findById').mockResolvedValue({} as any); jest.spyOn(memberRepo, 'findOne').mockResolvedValue({} as MemberEntity); jest.spyOn(memberRepo, 'save'); @@ -85,10 +97,31 @@ describe('MemberService test suite', () => { MemberAlreadyExistsException, ); - expect(roleRepo.findOne).toHaveBeenCalledTimes(1); + expect(roleService.findById).toHaveBeenCalledWith(roleId); + expect(userService.findById).toHaveBeenCalledWith(userId); expect(memberRepo.findOne).toHaveBeenCalledTimes(1); expect(memberRepo.save).not.toHaveBeenCalled(); }); + + it('creating a member fails with invalid user', async () => { + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); + jest + .spyOn(userService, 'findById') + .mockRejectedValue(new Error('User not found')); + jest.spyOn(memberRepo, 'findOne'); + jest.spyOn(memberRepo, 'save'); + + await expect(memberService.create(dto)).rejects.toThrow( + MemberInvalidUserException, + ); + + expect(roleService.findById).toHaveBeenCalledWith(roleId); + expect(userService.findById).toHaveBeenCalledWith(userId); + expect(memberRepo.findOne).not.toHaveBeenCalled(); + expect(memberRepo.save).not.toHaveBeenCalled(); + }); }); describe('createMany', () => { @@ -104,27 +137,26 @@ describe('MemberService test suite', () => { }); it('creating members succeeds with valid inputs', async () => { - jest - .spyOn(roleRepo, 'findOne') - .mockResolvedValue({ project: { id: projectId } } as RoleEntity); + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); + jest.spyOn(userService, 'findById').mockResolvedValue({} as any); jest.spyOn(memberRepo, 'findOne').mockResolvedValue(null); - jest.spyOn(memberRepo, 'save'); + jest.spyOn(memberRepo, 'save').mockResolvedValue([] as any); await memberService.createMany(dtos); - expect(roleRepo.findOne).toHaveBeenCalledTimes(memberCount); + expect(roleService.findById).toHaveBeenCalledTimes(memberCount); + expect(userService.findById).toHaveBeenCalledTimes(memberCount); expect(memberRepo.findOne).toHaveBeenCalledTimes(memberCount); expect(memberRepo.save).toHaveBeenCalledTimes(1); - expect(memberRepo.save).toHaveBeenCalledWith( - members.map(({ roleId, userId }) => - MemberEntity.from({ roleId, userId }), - ), - ); }); + it('creating members fails with an existent member', async () => { - jest - .spyOn(roleRepo, 'findOne') - .mockResolvedValue({ project: { id: projectId } } as RoleEntity); + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); + jest.spyOn(userService, 'findById').mockResolvedValue({} as any); jest.spyOn(memberRepo, 'findOne').mockResolvedValue({} as MemberEntity); jest.spyOn(memberRepo, 'save'); @@ -132,10 +164,31 @@ describe('MemberService test suite', () => { MemberAlreadyExistsException, ); - expect(roleRepo.findOne).toHaveBeenCalledTimes(1); + expect(roleService.findById).toHaveBeenCalledTimes(1); + expect(userService.findById).toHaveBeenCalledTimes(1); expect(memberRepo.findOne).toHaveBeenCalledTimes(1); expect(memberRepo.save).not.toHaveBeenCalled(); }); + + it('creating members fails with invalid user', async () => { + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); + jest + .spyOn(userService, 'findById') + .mockRejectedValue(new Error('User not found')); + jest.spyOn(memberRepo, 'findOne'); + jest.spyOn(memberRepo, 'save'); + + await expect(memberService.createMany(dtos)).rejects.toThrow( + MemberInvalidUserException, + ); + + expect(roleService.findById).toHaveBeenCalledTimes(1); + expect(userService.findById).toHaveBeenCalledTimes(1); + expect(memberRepo.findOne).not.toHaveBeenCalled(); + expect(memberRepo.save).not.toHaveBeenCalled(); + }); }); describe('update', () => { @@ -151,30 +204,33 @@ describe('MemberService test suite', () => { it('updating a member succeeds with valid inputs', async () => { const newRoleId = faker.number.int(); - jest.spyOn(roleRepo, 'findOne').mockResolvedValue({ + const role = { project: { id: projectId }, id: newRoleId, - } as RoleEntity); - jest.spyOn(memberRepo, 'findOne').mockResolvedValue({ + } as RoleEntity; + const member = { id: memberId, role: { id: roleId, project: { id: projectId } }, - } as MemberEntity); - jest.spyOn(memberRepo, 'save'); + } as MemberEntity; + + jest.spyOn(roleService, 'findById').mockResolvedValue(role); + jest.spyOn(memberRepo, 'findOne').mockResolvedValue(member); + jest.spyOn(memberRepo, 'save').mockResolvedValue({} as MemberEntity); await memberService.update(dto); - expect(roleRepo.findOne).toHaveBeenCalledTimes(1); - expect(memberRepo.findOne).toHaveBeenCalledTimes(1); - expect(memberRepo.save).toHaveBeenCalledTimes(1); - expect(memberRepo.save).toHaveBeenCalledWith({ - id: memberId, - role: { id: newRoleId, project: { id: projectId } }, + expect(roleService.findById).toHaveBeenCalledWith(roleId); + expect(memberRepo.findOne).toHaveBeenCalledWith({ + where: { id: memberId }, + relations: { role: { project: true } }, }); + expect(memberRepo.save).toHaveBeenCalledTimes(1); }); + it('updating a member fails with a nonexistent member', async () => { - jest - .spyOn(roleRepo, 'findOne') - .mockResolvedValue({ project: { id: projectId } } as RoleEntity); + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); jest.spyOn(memberRepo, 'findOne').mockResolvedValue(null); jest.spyOn(memberRepo, 'save'); @@ -182,14 +238,15 @@ describe('MemberService test suite', () => { MemberNotFoundException, ); - expect(roleRepo.findOne).toHaveBeenCalledTimes(1); + expect(roleService.findById).toHaveBeenCalledWith(roleId); expect(memberRepo.findOne).toHaveBeenCalledTimes(1); expect(memberRepo.save).not.toHaveBeenCalled(); }); - it('updating a member fails with not matching inputs', async () => { - jest - .spyOn(roleRepo, 'findOne') - .mockResolvedValue({ project: { id: projectId } } as RoleEntity); + + it('updating a member fails with not matching project', async () => { + jest.spyOn(roleService, 'findById').mockResolvedValue({ + project: { id: projectId }, + } as RoleEntity); jest.spyOn(memberRepo, 'findOne').mockResolvedValue({ role: { id: roleId, project: { id: faker.number.int() } }, } as MemberEntity); @@ -199,9 +256,287 @@ describe('MemberService test suite', () => { MemberUpdateRoleNotMatchedProjectException, ); - expect(roleRepo.findOne).toHaveBeenCalledTimes(1); + expect(roleService.findById).toHaveBeenCalledWith(roleId); expect(memberRepo.findOne).toHaveBeenCalledTimes(1); expect(memberRepo.save).not.toHaveBeenCalled(); }); }); + + describe('validateEmail', () => { + it('validates email successfully when no tenants exist', async () => { + jest.spyOn(tenantRepo, 'find').mockResolvedValue([]); + + const result = await memberService.validateEmail('test@example.com'); + + expect(result).toBe(true); + expect(tenantRepo.find).toHaveBeenCalledTimes(1); + }); + + it('validates email successfully when tenant has no allowDomains', async () => { + const tenant = { allowDomains: null } as TenantEntity; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenant]); + + const result = await memberService.validateEmail('test@example.com'); + + expect(result).toBe(true); + expect(tenantRepo.find).toHaveBeenCalledTimes(1); + }); + + it('validates email successfully when tenant has empty allowDomains', async () => { + const tenant = { allowDomains: [] } as unknown as TenantEntity; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenant]); + + const result = await memberService.validateEmail('test@example.com'); + + expect(result).toBe(true); + expect(tenantRepo.find).toHaveBeenCalledTimes(1); + }); + + it('validates email successfully when domain is allowed', async () => { + const tenant = { + allowDomains: ['example.com', 'test.com'], + } as TenantEntity; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenant]); + + const result = await memberService.validateEmail('test@example.com'); + + expect(result).toBe(true); + expect(tenantRepo.find).toHaveBeenCalledTimes(1); + }); + + it('validates email fails when domain is not allowed', async () => { + const tenant = { + allowDomains: ['example.com', 'test.com'], + } as TenantEntity; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenant]); + + await expect( + memberService.validateEmail('test@forbidden.com'), + ).rejects.toThrow(NotAllowedDomainException); + + expect(tenantRepo.find).toHaveBeenCalledTimes(1); + }); + }); + + describe('findByProjectId', () => { + const projectId = faker.number.int(); + const sort = { createdAt: SortMethodEnum.ASC }; + + it('finds members by project id successfully', async () => { + const members = [ + { id: 1, role: { id: 1 }, user: { id: 1 } }, + { id: 2, role: { id: 2 }, user: { id: 2 } }, + ] as MemberEntity[]; + const total = 2; + + jest + .spyOn(memberRepo, 'findAndCount') + .mockResolvedValue([members, total]); + + const result = await memberService.findByProjectId({ projectId, sort }); + + expect(result).toEqual({ members, total }); + expect(memberRepo.findAndCount).toHaveBeenCalledWith({ + where: { role: { project: { id: projectId } } }, + order: { createdAt: sort.createdAt }, + relations: { role: true, user: true }, + }); + }); + + it('finds members by project id with DESC sort', async () => { + const sortDesc = { createdAt: SortMethodEnum.DESC }; + const members = [] as MemberEntity[]; + const total = 0; + + jest + .spyOn(memberRepo, 'findAndCount') + .mockResolvedValue([members, total]); + + const result = await memberService.findByProjectId({ + projectId, + sort: sortDesc, + }); + + expect(result).toEqual({ members, total }); + expect(memberRepo.findAndCount).toHaveBeenCalledWith({ + where: { role: { project: { id: projectId } } }, + order: { createdAt: sortDesc.createdAt }, + relations: { role: true, user: true }, + }); + }); + }); + + describe('findAll', () => { + const projectId = faker.number.int(); + const page = 1; + const limit = 10; + + it('finds all members with basic parameters', async () => { + const items = [ + { id: 1, role: { id: 1 }, user: { id: 1 } }, + { id: 2, role: { id: 2 }, user: { id: 2 } }, + ] as MemberEntity[]; + const total = 2; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(items), + getCount: jest.fn().mockResolvedValue(total), + }; + + jest + .spyOn(memberRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const dto: FindAllMembersDto = { + projectId, + options: { page, limit }, + }; + + const result = await memberService.findAll(dto); + + expect(result).toEqual({ + items, + meta: { + itemCount: items.length, + totalItems: total, + itemsPerPage: limit, + currentPage: page, + totalPages: Math.ceil(total / limit), + }, + }); + }); + + it('finds all members with queries and AND operator', async () => { + const items = [] as MemberEntity[]; + const total = 0; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(items), + getCount: jest.fn().mockResolvedValue(total), + }; + + jest + .spyOn(memberRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const dto: FindAllMembersDto = { + projectId, + options: { page, limit }, + queries: [ + { key: 'name', value: 'John', condition: 'IS' as any }, + { + key: 'email', + value: 'john@example.com', + condition: 'CONTAINS' as any, + }, + ], + operator: 'AND', + }; + + const result = await memberService.findAll(dto); + + expect(result).toEqual({ + items, + meta: { + itemCount: items.length, + totalItems: total, + itemsPerPage: limit, + currentPage: page, + totalPages: Math.ceil(total / limit), + }, + }); + }); + + it('finds all members with queries and OR operator', async () => { + const items = [] as MemberEntity[]; + const total = 0; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(items), + getCount: jest.fn().mockResolvedValue(total), + }; + + jest + .spyOn(memberRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const dto: FindAllMembersDto = { + projectId, + options: { page, limit }, + queries: [{ key: 'role', value: 'admin', condition: 'IS' as any }], + operator: 'OR', + }; + + const result = await memberService.findAll(dto); + + expect(result).toEqual({ + items, + meta: { + itemCount: items.length, + totalItems: total, + itemsPerPage: limit, + currentPage: page, + totalPages: Math.ceil(total / limit), + }, + }); + }); + }); + + describe('delete', () => { + const memberId = faker.number.int(); + + it('deletes a member successfully', async () => { + jest.spyOn(memberRepo, 'remove').mockResolvedValue({} as MemberEntity); + + await memberService.delete(memberId); + + expect(memberRepo.remove).toHaveBeenCalledWith( + expect.objectContaining({ id: memberId }), + ); + }); + }); + + describe('deleteMany', () => { + const memberIds = [ + faker.number.int(), + faker.number.int(), + faker.number.int(), + ]; + const dto: DeleteManyMemberRequestDto = { memberIds }; + + it('deletes multiple members successfully', async () => { + const mockQueryBuilder = { + delete: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + execute: jest.fn().mockResolvedValue({ affected: memberIds.length }), + }; + + jest + .spyOn(memberRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + await memberService.deleteMany(dto); + + expect(memberRepo.createQueryBuilder).toHaveBeenCalledTimes(1); + expect(mockQueryBuilder.delete).toHaveBeenCalledTimes(1); + expect(mockQueryBuilder.from).toHaveBeenCalledWith(MemberEntity); + expect(mockQueryBuilder.where).toHaveBeenCalledWith('id IN (:...ids)', { + ids: memberIds, + }); + expect(mockQueryBuilder.execute).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/apps/api/src/domains/admin/project/project/project.controller.spec.ts b/apps/api/src/domains/admin/project/project/project.controller.spec.ts index 3e6cdc09e..7e75482fa 100644 --- a/apps/api/src/domains/admin/project/project/project.controller.spec.ts +++ b/apps/api/src/domains/admin/project/project/project.controller.spec.ts @@ -24,6 +24,7 @@ import { IssueService } from '../issue/issue.service'; import { CreateProjectRequestDto, FindProjectsRequestDto, + UpdateProjectRequestDto, } from './dtos/requests'; import { ProjectController } from './project.controller'; import { ProjectService } from './project.service'; @@ -31,7 +32,10 @@ import { ProjectService } from './project.service'; const MockProjectService = { create: jest.fn(), findAll: jest.fn(), + findById: jest.fn(), + update: jest.fn(), deleteById: jest.fn(), + checkName: jest.fn(), }; const MockFeedbackService = { countByProjectId: jest.fn(), @@ -44,6 +48,9 @@ describe('ProjectController', () => { let projectController: ProjectController; beforeEach(async () => { + // Reset all mocks before each test + jest.clearAllMocks(); + const module = await Test.createTestingModule({ controllers: [ProjectController], providers: [ @@ -58,55 +65,461 @@ describe('ProjectController', () => { }); describe('create', () => { - it('', async () => { - jest.spyOn(MockProjectService, 'create'); + it('should create a new project successfully', async () => { + const mockProject = { + id: faker.number.int(), + name: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + createdAt: faker.date.past(), + }; + + MockProjectService.create.mockResolvedValue(mockProject); + const dto = new CreateProjectRequestDto(); - dto.name = faker.string.sample(); - dto.description = faker.string.sample(); + dto.name = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.timezone = { + countryCode: faker.location.countryCode(), + name: faker.location.timeZone(), + offset: '+09:00', + }; + + const result = await projectController.create(dto); - await projectController.create(dto); expect(MockProjectService.create).toHaveBeenCalledTimes(1); + expect(MockProjectService.create).toHaveBeenCalledWith(dto); + expect(result).toBeDefined(); + }); + + it('should handle project creation with optional fields', async () => { + const mockProject = { + id: faker.number.int(), + name: faker.string.alphanumeric(10), + description: null, + createdAt: faker.date.past(), + }; + + MockProjectService.create.mockResolvedValue(mockProject); + + const dto = new CreateProjectRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.description = null; + dto.timezone = { + countryCode: faker.location.countryCode(), + name: faker.location.timeZone(), + offset: '+09:00', + }; + dto.roles = []; + dto.members = []; + dto.apiKeys = []; + + const result = await projectController.create(dto); + + expect(MockProjectService.create).toHaveBeenCalledTimes(1); + expect(MockProjectService.create).toHaveBeenCalledWith(dto); + expect(result).toBeDefined(); }); }); describe('findAll', () => { - it('', async () => { - jest.spyOn(MockProjectService, 'findAll'); + it('should return paginated projects list', async () => { + const mockProjects = { + items: [ + { + id: faker.number.int(), + name: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + createdAt: faker.date.past(), + }, + ], + meta: { + itemCount: 1, + totalItems: 1, + itemsPerPage: 10, + totalPages: 1, + currentPage: 1, + }, + }; + + MockProjectService.findAll.mockResolvedValue(mockProjects); + const dto = new FindProjectsRequestDto(); - dto.limit = faker.number.int(); - dto.page = faker.number.int(); + dto.limit = 10; + dto.page = 1; + dto.searchText = faker.string.alphanumeric(5); + const userDto = new UserDto(); + userDto.id = faker.number.int(); + userDto.type = 'SUPER' as any; + + const result = await projectController.findAll(dto, userDto); + + expect(MockProjectService.findAll).toHaveBeenCalledTimes(1); + expect(MockProjectService.findAll).toHaveBeenCalledWith({ + user: userDto, + options: { limit: dto.limit, page: dto.page }, + searchText: dto.searchText, + }); + expect(result).toBeDefined(); + }); + + it('should handle findAll without searchText', async () => { + const mockProjects = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 1, + }, + }; + + MockProjectService.findAll.mockResolvedValue(mockProjects); + + const dto = new FindProjectsRequestDto(); + dto.limit = 10; + dto.page = 1; + + const userDto = new UserDto(); + userDto.id = faker.number.int(); + userDto.type = 'SUPER' as any; + + const result = await projectController.findAll(dto, userDto); - await projectController.findAll(dto, userDto); expect(MockProjectService.findAll).toHaveBeenCalledTimes(1); + expect(MockProjectService.findAll).toHaveBeenCalledWith({ + user: userDto, + options: { limit: dto.limit, page: dto.page }, + searchText: undefined, + }); + expect(result).toBeDefined(); }); }); + describe('checkName', () => { + it('should check if project name exists', async () => { + const projectName = faker.string.alphanumeric(10); + const mockResult = true; + + MockProjectService.checkName.mockResolvedValue(mockResult); + + const result = await projectController.checkName(projectName); + + expect(MockProjectService.checkName).toHaveBeenCalledTimes(1); + expect(MockProjectService.checkName).toHaveBeenCalledWith(projectName); + expect(result).toBe(mockResult); + }); + + it('should return false when project name does not exist', async () => { + const projectName = faker.string.alphanumeric(10); + const mockResult = false; + + MockProjectService.checkName.mockResolvedValue(mockResult); + + const result = await projectController.checkName(projectName); + + expect(MockProjectService.checkName).toHaveBeenCalledTimes(1); + expect(MockProjectService.checkName).toHaveBeenCalledWith(projectName); + expect(result).toBe(mockResult); + }); + }); + + describe('findOne', () => { + it('should return a project by id', async () => { + const projectId = faker.number.int(); + const mockProject = { + id: projectId, + name: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + createdAt: faker.date.past(), + }; + + MockProjectService.findById.mockResolvedValue(mockProject); + + const result = await projectController.findOne(projectId); + + expect(MockProjectService.findById).toHaveBeenCalledTimes(1); + expect(MockProjectService.findById).toHaveBeenCalledWith({ projectId }); + expect(result).toBeDefined(); + }); + }); + describe('countFeedbacks', () => { it('should return a number of total feedbacks by project id', async () => { - jest.spyOn(MockFeedbackService, 'countByProjectId'); const projectId = faker.number.int(); + const mockCount = faker.number.int({ min: 0, max: 100 }); + + MockFeedbackService.countByProjectId.mockResolvedValue(mockCount); + + const result = await projectController.countFeedbacks(projectId); - await projectController.countFeedbacks(projectId); expect(MockFeedbackService.countByProjectId).toHaveBeenCalledTimes(1); + expect(MockFeedbackService.countByProjectId).toHaveBeenCalledWith({ + projectId, + }); + expect(result).toBeDefined(); }); }); describe('countIssues', () => { it('should return a number of total issues by project id', async () => { - jest.spyOn(MockIssueService, 'countByProjectId'); const projectId = faker.number.int(); + const mockCount = faker.number.int({ min: 0, max: 100 }); + + MockIssueService.countByProjectId.mockResolvedValue(mockCount); + + const result = await projectController.countIssues(projectId); - await projectController.countIssues(projectId); expect(MockIssueService.countByProjectId).toHaveBeenCalledTimes(1); + expect(MockIssueService.countByProjectId).toHaveBeenCalledWith({ + projectId, + }); + expect(result).toBeDefined(); }); }); - describe('delete', () => { - it('', async () => { - jest.spyOn(MockProjectService, 'deleteById'); + describe('updateOne', () => { + it('should update a project successfully', async () => { + const projectId = faker.number.int(); + const mockUpdatedProject = { + id: projectId, + name: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + updatedAt: faker.date.recent(), + }; + + MockProjectService.update.mockResolvedValue(mockUpdatedProject); + + const dto = new UpdateProjectRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.timezone = { + countryCode: faker.location.countryCode(), + name: faker.location.timeZone(), + offset: '+09:00', + }; + + const result = await projectController.updateOne(projectId, dto); + + expect(MockProjectService.update).toHaveBeenCalledTimes(1); + expect(MockProjectService.update).toHaveBeenCalledWith({ + ...dto, + projectId, + }); + expect(result).toBeDefined(); + }); + + it('should handle project update with minimal data', async () => { const projectId = faker.number.int(); + const mockUpdatedProject = { + id: projectId, + name: faker.string.alphanumeric(10), + description: null, + updatedAt: faker.date.recent(), + }; + + MockProjectService.update.mockResolvedValue(mockUpdatedProject); + + const dto = new UpdateProjectRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.description = null; + dto.timezone = { + countryCode: faker.location.countryCode(), + name: faker.location.timeZone(), + offset: '+09:00', + }; + + const result = await projectController.updateOne(projectId, dto); + + expect(MockProjectService.update).toHaveBeenCalledTimes(1); + expect(MockProjectService.update).toHaveBeenCalledWith({ + ...dto, + projectId, + }); + expect(result).toBeDefined(); + }); + }); + + describe('Error Cases', () => { + describe('create', () => { + it('should handle service errors during project creation', async () => { + const error = new Error('Project creation failed'); + MockProjectService.create.mockRejectedValue(error); + + const dto = new CreateProjectRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.timezone = { + countryCode: faker.location.countryCode(), + name: faker.location.timeZone(), + offset: '+09:00', + }; + + await expect(projectController.create(dto)).rejects.toThrow(error); + expect(MockProjectService.create).toHaveBeenCalledTimes(1); + }); + }); + + describe('findOne', () => { + it('should handle project not found error', async () => { + const projectId = faker.number.int(); + const error = new Error('Project not found'); + MockProjectService.findById.mockRejectedValue(error); + + await expect(projectController.findOne(projectId)).rejects.toThrow( + error, + ); + expect(MockProjectService.findById).toHaveBeenCalledTimes(1); + }); + }); + + describe('updateOne', () => { + it('should handle service errors during project update', async () => { + const projectId = faker.number.int(); + const error = new Error('Project update failed'); + MockProjectService.update.mockRejectedValue(error); + + const dto = new UpdateProjectRequestDto(); + dto.name = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.timezone = { + countryCode: faker.location.countryCode(), + name: faker.location.timeZone(), + offset: '+09:00', + }; + + await expect( + projectController.updateOne(projectId, dto), + ).rejects.toThrow(error); + expect(MockProjectService.update).toHaveBeenCalledTimes(1); + }); + }); + + describe('delete', () => { + it('should handle service errors during project deletion', async () => { + const projectId = faker.number.int(); + const error = new Error('Project deletion failed'); + MockProjectService.deleteById.mockRejectedValue(error); + + await expect(projectController.delete(projectId)).rejects.toThrow( + error, + ); + expect(MockProjectService.deleteById).toHaveBeenCalledTimes(1); + }); + }); + + describe('countFeedbacks', () => { + it('should handle service errors when counting feedbacks', async () => { + const projectId = faker.number.int(); + const error = new Error('Failed to count feedbacks'); + MockFeedbackService.countByProjectId.mockRejectedValue(error); + + await expect( + projectController.countFeedbacks(projectId), + ).rejects.toThrow(error); + expect(MockFeedbackService.countByProjectId).toHaveBeenCalledTimes(1); + }); + }); + + describe('countIssues', () => { + it('should handle service errors when counting issues', async () => { + const projectId = faker.number.int(); + const error = new Error('Failed to count issues'); + MockIssueService.countByProjectId.mockRejectedValue(error); + + await expect(projectController.countIssues(projectId)).rejects.toThrow( + error, + ); + expect(MockIssueService.countByProjectId).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('Edge Cases', () => { + describe('findAll', () => { + it('should handle empty search results', async () => { + const mockEmptyResult = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 1, + }, + }; + + MockProjectService.findAll.mockResolvedValue(mockEmptyResult); + + const dto = new FindProjectsRequestDto(); + dto.limit = 10; + dto.page = 1; + dto.searchText = 'nonexistent'; + + const userDto = new UserDto(); + userDto.id = faker.number.int(); + userDto.type = 'SUPER' as any; + + const result = await projectController.findAll(dto, userDto); + + expect(MockProjectService.findAll).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + }); + + it('should handle large page numbers', async () => { + const mockResult = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 10, + totalPages: 0, + currentPage: 999, + }, + }; + + MockProjectService.findAll.mockResolvedValue(mockResult); + + const dto = new FindProjectsRequestDto(); + dto.limit = 10; + dto.page = 999; + + const userDto = new UserDto(); + userDto.id = faker.number.int(); + userDto.type = 'SUPER' as any; + + const result = await projectController.findAll(dto, userDto); + + expect(MockProjectService.findAll).toHaveBeenCalledTimes(1); + expect(result).toBeDefined(); + }); + }); + + describe('checkName', () => { + it('should handle empty string project name', async () => { + const projectName = ''; + const mockResult = false; + + MockProjectService.checkName.mockResolvedValue(mockResult); + + const result = await projectController.checkName(projectName); + + expect(MockProjectService.checkName).toHaveBeenCalledTimes(1); + expect(MockProjectService.checkName).toHaveBeenCalledWith(projectName); + expect(result).toBe(mockResult); + }); + + it('should handle very long project name', async () => { + const projectName = 'a'.repeat(100); + const mockResult = false; + + MockProjectService.checkName.mockResolvedValue(mockResult); + + const result = await projectController.checkName(projectName); - await projectController.delete(projectId); - expect(MockProjectService.deleteById).toHaveBeenCalledTimes(1); + expect(MockProjectService.checkName).toHaveBeenCalledTimes(1); + expect(MockProjectService.checkName).toHaveBeenCalledWith(projectName); + expect(result).toBe(mockResult); + }); }); }); }); diff --git a/apps/api/src/domains/admin/project/project/project.service.spec.ts b/apps/api/src/domains/admin/project/project/project.service.spec.ts index 4daade934..3d82e31ac 100644 --- a/apps/api/src/domains/admin/project/project/project.service.spec.ts +++ b/apps/api/src/domains/admin/project/project/project.service.spec.ts @@ -14,6 +14,8 @@ * under the License. */ import { faker } from '@faker-js/faker'; +import { BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import type { Repository } from 'typeorm'; @@ -25,6 +27,9 @@ import { } from '@/test-utils/fixtures'; import { getRandomEnumValues, TestConfig } from '@/test-utils/util-functions'; import { ProjectServiceProviders } from '../../../../test-utils/providers/project.service.providers'; +import { ChannelEntity } from '../../channel/channel/channel.entity'; +import { TenantNotFoundException } from '../../tenant/exceptions'; +import { TenantEntity } from '../../tenant/tenant.entity'; import { UserDto } from '../../user/dtos'; import { UserTypeEnum } from '../../user/entities/enums'; import { ApiKeyEntity } from '../api-key/api-key.entity'; @@ -38,6 +43,7 @@ import { ProjectInvalidNameException, ProjectNotFoundException, } from './exceptions'; +import type { Timezone } from './project.entity'; import { ProjectEntity } from './project.entity'; import { ProjectService } from './project.service'; @@ -47,6 +53,9 @@ describe('ProjectService Test suite', () => { let roleRepo: Repository; let memberRepo: Repository; let apiKeyRepo: Repository; + let tenantRepo: Repository; + let channelRepo: Repository; + let configService: ConfigService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -59,6 +68,66 @@ describe('ProjectService Test suite', () => { roleRepo = module.get(getRepositoryToken(RoleEntity)); memberRepo = module.get(getRepositoryToken(MemberEntity)); apiKeyRepo = module.get(getRepositoryToken(ApiKeyEntity)); + tenantRepo = module.get(getRepositoryToken(TenantEntity)); + channelRepo = module.get(getRepositoryToken(ChannelEntity)); + configService = module.get(ConfigService); + }); + + describe('checkName', () => { + it('should return true when project name exists', async () => { + const projectName = faker.string.sample(); + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(projectFixture); + + const result = await projectService.checkName(projectName); + + expect(result).toBe(true); + expect(projectRepo.findOneBy).toHaveBeenCalledWith({ name: projectName }); + }); + + it('should return false when project name does not exist', async () => { + const projectName = faker.string.sample(); + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(null); + + const result = await projectService.checkName(projectName); + + expect(result).toBe(false); + expect(projectRepo.findOneBy).toHaveBeenCalledWith({ name: projectName }); + }); + }); + + describe('findTenant', () => { + it('should return tenant when tenant exists', async () => { + const tenant = { + id: faker.number.int(), + siteName: faker.string.sample(), + description: faker.string.sample(), + useEmail: true, + allowDomains: [], + useOAuth: false, + oauthConfig: null, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: undefined, + projects: [], + beforeInsertHook: jest.fn(), + beforeUpdateHook: jest.fn(), + }; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenant as any]); + + const result = await projectService.findTenant(); + + expect(result).toEqual(tenant); + expect(tenantRepo.find).toHaveBeenCalledTimes(1); + }); + + it('should throw TenantNotFoundException when no tenant exists', async () => { + jest.spyOn(tenantRepo, 'find').mockResolvedValue([]); + + await expect(projectService.findTenant()).rejects.toThrow( + TenantNotFoundException, + ); + expect(tenantRepo.find).toHaveBeenCalledTimes(1); + }); }); describe('create', () => { @@ -216,6 +285,81 @@ describe('ProjectService Test suite', () => { ProjectAlreadyExistsException, ); }); + + it('creating a project succeeds with default roles when no roles provided', async () => { + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(tenantRepo, 'find').mockResolvedValue([ + { + id: faker.number.int(), + siteName: faker.string.sample(), + description: faker.string.sample(), + useEmail: true, + allowDomains: [], + useOAuth: false, + oauthConfig: null, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: undefined, + projects: [], + beforeInsertHook: jest.fn(), + beforeUpdateHook: jest.fn(), + } as any, + ]); + + const project = await projectService.create(dto); + + expect(project.id).toEqual(projectId); + expect(project.name).toEqual(name); + expect(project.description).toEqual(description); + expect(project.roles).toHaveLength(3); // Admin, Editor, Viewer + }); + + it('creating a project fails with invalid role name in members', async () => { + dto.roles = [ + { + name: roleFixture.name, + permissions: getRandomEnumValues(PermissionEnum), + }, + ]; + dto.members = [ + { + roleName: 'INVALID_ROLE_NAME', + userId: userFixture.id, + }, + ]; + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(tenantRepo, 'find').mockResolvedValue([ + { + id: faker.number.int(), + siteName: faker.string.sample(), + description: faker.string.sample(), + useEmail: true, + allowDomains: [], + useOAuth: false, + oauthConfig: null, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: undefined, + projects: [], + beforeInsertHook: jest.fn(), + beforeUpdateHook: jest.fn(), + } as any, + ]); + + await expect(projectService.create(dto)).rejects.toThrow( + BadRequestException, + ); + }); + + it('creating a project fails when tenant not found', async () => { + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(tenantRepo, 'find').mockResolvedValue([]); + + await expect(projectService.create(dto)).rejects.toThrow( + TenantNotFoundException, + ); + }); }); describe('findAll', () => { let dto: FindAllProjectsDto; @@ -242,6 +386,26 @@ describe('ProjectService Test suite', () => { expect(meta.totalItems).toEqual(1); }); + + it('finding all projects succeeds with empty search text', async () => { + dto.user = new UserDto(); + dto.user.type = UserTypeEnum.SUPER; + dto.searchText = ''; + + const { meta } = await projectService.findAll(dto); + + expect(meta.totalItems).toEqual(1); + }); + + it('finding all projects succeeds with different pagination options', async () => { + dto.user = new UserDto(); + dto.user.type = UserTypeEnum.SUPER; + dto.options = { limit: 5, page: 2 }; + + const { meta } = await projectService.findAll(dto); + + expect(meta.totalItems).toBeGreaterThanOrEqual(1); + }); }); describe('findById', () => { let dto: FindByProjectIdDto; @@ -262,6 +426,17 @@ describe('ProjectService Test suite', () => { ProjectNotFoundException, ); }); + + it('finding a project by an id succeeds with valid project data', async () => { + const projectId = projectFixture.id; + dto.projectId = projectId; + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(projectFixture); + + const project = await projectService.findById(dto); + + expect(project.id).toEqual(projectId); + expect(projectRepo.findOneBy).toHaveBeenCalledWith({ id: projectId }); + }); }); describe('update ', () => { const description = faker.string.sample(); @@ -291,6 +466,31 @@ describe('ProjectService Test suite', () => { expect(projectRepo.save).not.toHaveBeenCalled(); }); + + it('updating a project succeeds with timezone', async () => { + const timezone: Timezone = { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }; + dto.timezone = timezone; + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(projectRepo, 'save'); + + await projectService.update(dto); + + expect(projectRepo.save).toHaveBeenCalledTimes(1); + }); + + it('updating a project fails with invalid project id', async () => { + const invalidProjectId = faker.number.int(); + dto.projectId = invalidProjectId; + jest.spyOn(projectRepo, 'findOneBy').mockResolvedValue(null); + + await expect(projectService.update(dto)).rejects.toThrow( + ProjectNotFoundException, + ); + }); }); describe('deleteById', () => { it('deleting a project succeeds with a valid id', async () => { @@ -301,5 +501,59 @@ describe('ProjectService Test suite', () => { expect(projectRepo.remove).toHaveBeenCalledTimes(1); }); + + it('deleting a project succeeds with OpenSearch enabled', async () => { + const projectId = faker.number.int(); + const channels = [ + { + id: faker.number.int(), + name: faker.string.sample(), + description: faker.string.sample(), + imageConfig: null, + feedbackSearchMaxDays: 30, + project: null, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: undefined, + }, + { + id: faker.number.int(), + name: faker.string.sample(), + description: faker.string.sample(), + imageConfig: null, + feedbackSearchMaxDays: 30, + project: null, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: undefined, + }, + ]; + + jest.spyOn(configService, 'get').mockReturnValue(true); + jest.spyOn(channelRepo, 'find').mockResolvedValue(channels as any); + jest.spyOn(projectRepo, 'remove'); + + await projectService.deleteById(projectId); + + expect(configService.get).toHaveBeenCalledWith('opensearch.use'); + expect(channelRepo.find).toHaveBeenCalledWith({ + where: { project: { id: projectId } }, + }); + expect(projectRepo.remove).toHaveBeenCalledTimes(1); + }); + + it('deleting a project succeeds with OpenSearch disabled', async () => { + const projectId = faker.number.int(); + + jest.spyOn(configService, 'get').mockReturnValue(false); + jest.spyOn(channelRepo, 'find'); + jest.spyOn(projectRepo, 'remove'); + + await projectService.deleteById(projectId); + + expect(configService.get).toHaveBeenCalledWith('opensearch.use'); + expect(channelRepo.find).not.toHaveBeenCalled(); + expect(projectRepo.remove).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/apps/api/src/domains/admin/project/role/dtos/requests/create-role-request.dto.ts b/apps/api/src/domains/admin/project/role/dtos/requests/create-role-request.dto.ts index ad68ad644..9dd7946ce 100644 --- a/apps/api/src/domains/admin/project/role/dtos/requests/create-role-request.dto.ts +++ b/apps/api/src/domains/admin/project/role/dtos/requests/create-role-request.dto.ts @@ -14,7 +14,7 @@ * under the License. */ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { ArrayDistinct } from '@/common/validators'; import { PermissionEnum } from '../../permission.enum'; @@ -27,5 +27,6 @@ export class CreateRoleRequestDto { @ApiProperty() @IsEnum(PermissionEnum, { each: true }) @ArrayDistinct() + @IsNotEmpty() permissions: PermissionEnum[]; } diff --git a/apps/api/src/domains/admin/project/role/role.controller.spec.ts b/apps/api/src/domains/admin/project/role/role.controller.spec.ts index b37dcc851..8a156902d 100644 --- a/apps/api/src/domains/admin/project/role/role.controller.spec.ts +++ b/apps/api/src/domains/admin/project/role/role.controller.spec.ts @@ -23,6 +23,10 @@ import { MockDataSource, } from '@/test-utils/util-functions'; import { CreateRoleDto, UpdateRoleDto } from './dtos'; +import { + RoleAlreadyExistsException, + RoleNotFoundException, +} from './exceptions'; import { PermissionEnum } from './permission.enum'; import { RoleController } from './role.controller'; import { RoleService } from './role.service'; @@ -49,69 +53,251 @@ describe('Role Controller', () => { controller = module.get(RoleController); }); + afterEach(() => { + jest.resetAllMocks(); + }); + it('to be defined', () => { expect(controller).toBeDefined(); }); - beforeEach(() => { - jest.resetAllMocks(); - }); - it('getAllRolesByProjectId', async () => { - const total = faker.number.int({ min: 0, max: 10 }); - const roles = Array.from({ length: total }).map(() => ({ - _id: faker.number.int(), - id: faker.number.int(), - name: faker.string.sample(), - permissions: [getRandomEnumValue(PermissionEnum)], - __v: faker.number.int({ max: 100, min: 0 }), - [faker.string.sample()]: faker.string.sample(), - })); - const projectId = faker.number.int(); - jest.spyOn(MockRoleService, 'findByProjectId').mockResolvedValue({ - roles, - total, + describe('getAllRolesByProjectId', () => { + it('should return all roles for a project successfully', async () => { + const total = faker.number.int({ min: 1, max: 10 }); + const roles = Array.from({ length: total }).map(() => ({ + _id: faker.number.int(), + id: faker.number.int(), + name: faker.string.sample(), + permissions: [getRandomEnumValue(PermissionEnum)], + __v: faker.number.int({ max: 100, min: 0 }), + [faker.string.sample()]: faker.string.sample(), + })); + const projectId = faker.number.int(); + jest.spyOn(MockRoleService, 'findByProjectId').mockResolvedValue({ + roles, + total, + }); + + const res = await controller.getAllRolesByProjectId(projectId); + + expect(MockRoleService.findByProjectId).toHaveBeenCalledTimes(1); + expect(MockRoleService.findByProjectId).toHaveBeenCalledWith(projectId); + expect(res).toEqual({ + total, + roles: roles.map(({ id, name, permissions }) => ({ + id, + name, + permissions, + })), + }); + }); + + it('should return empty result when no roles exist for project', async () => { + const projectId = faker.number.int(); + jest.spyOn(MockRoleService, 'findByProjectId').mockResolvedValue({ + roles: [], + total: 0, + }); + + const res = await controller.getAllRolesByProjectId(projectId); + + expect(MockRoleService.findByProjectId).toHaveBeenCalledTimes(1); + expect(MockRoleService.findByProjectId).toHaveBeenCalledWith(projectId); + expect(res).toEqual({ + total: 0, + roles: [], + }); }); - const res = await controller.getAllRolesByProjectId(projectId); + it('should handle service errors properly', async () => { + const projectId = faker.number.int(); + const error = new Error('Database connection failed'); + jest.spyOn(MockRoleService, 'findByProjectId').mockRejectedValue(error); - expect(MockRoleService.findByProjectId).toHaveBeenCalledTimes(1); - expect(res).toEqual({ - total, - roles: roles.map(({ id, name, permissions }) => ({ - id, - name, - permissions, - })), + await expect( + controller.getAllRolesByProjectId(projectId), + ).rejects.toThrow('Database connection failed'); + expect(MockRoleService.findByProjectId).toHaveBeenCalledTimes(1); + expect(MockRoleService.findByProjectId).toHaveBeenCalledWith(projectId); }); }); - it('createRole', async () => { - const dto = new CreateRoleDto(); - const projectId = faker.number.int(); + describe('createRole', () => { + it('should create a role successfully', async () => { + const dto = new CreateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [getRandomEnumValue(PermissionEnum)]; + const projectId = faker.number.int(); + + jest.spyOn(MockRoleService, 'create').mockResolvedValue(undefined); - await controller.createRole(projectId, dto); + await controller.createRole(projectId, dto); - expect(MockRoleService.create).toHaveBeenCalledTimes(1); - expect(MockRoleService.create).toHaveBeenCalledWith({ ...dto, projectId }); + expect(MockRoleService.create).toHaveBeenCalledTimes(1); + expect(MockRoleService.create).toHaveBeenCalledWith({ + ...dto, + projectId, + }); + }); + + it('should throw RoleAlreadyExistsException when role name already exists', async () => { + const dto = new CreateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [getRandomEnumValue(PermissionEnum)]; + const projectId = faker.number.int(); + + jest + .spyOn(MockRoleService, 'create') + .mockRejectedValue(new RoleAlreadyExistsException()); + + await expect(controller.createRole(projectId, dto)).rejects.toThrow( + RoleAlreadyExistsException, + ); + expect(MockRoleService.create).toHaveBeenCalledTimes(1); + expect(MockRoleService.create).toHaveBeenCalledWith({ + ...dto, + projectId, + }); + }); + + it('should handle service errors properly', async () => { + const dto = new CreateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [getRandomEnumValue(PermissionEnum)]; + const projectId = faker.number.int(); + const error = new Error('Database error'); + + jest.spyOn(MockRoleService, 'create').mockRejectedValue(error); + + await expect(controller.createRole(projectId, dto)).rejects.toThrow( + 'Database error', + ); + expect(MockRoleService.create).toHaveBeenCalledTimes(1); + expect(MockRoleService.create).toHaveBeenCalledWith({ + ...dto, + projectId, + }); + }); }); - it('updateRole', async () => { - const dto = new UpdateRoleDto(); - const id = faker.number.int(); - const projectId = faker.number.int(); + describe('updateRole', () => { + it('should update a role successfully', async () => { + const dto = new UpdateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [getRandomEnumValue(PermissionEnum)]; + const id = faker.number.int(); + const projectId = faker.number.int(); + + jest.spyOn(MockRoleService, 'update').mockResolvedValue(undefined); + + await controller.updateRole(projectId, id, dto); + + expect(MockRoleService.update).toHaveBeenCalledTimes(1); + expect(MockRoleService.update).toHaveBeenCalledWith(id, projectId, dto); + }); + + it('should throw RoleNotFoundException when role does not exist', async () => { + const dto = new UpdateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [getRandomEnumValue(PermissionEnum)]; + const id = faker.number.int(); + const projectId = faker.number.int(); - await controller.updateRole(projectId, id, dto); + jest + .spyOn(MockRoleService, 'update') + .mockRejectedValue(new RoleNotFoundException()); + + await expect(controller.updateRole(projectId, id, dto)).rejects.toThrow( + RoleNotFoundException, + ); + expect(MockRoleService.update).toHaveBeenCalledTimes(1); + expect(MockRoleService.update).toHaveBeenCalledWith(id, projectId, dto); + }); - expect(MockRoleService.update).toHaveBeenCalledTimes(1); - expect(MockRoleService.update).toHaveBeenCalledWith(id, projectId, dto); + it('should throw RoleAlreadyExistsException when role name already exists', async () => { + const dto = new UpdateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [getRandomEnumValue(PermissionEnum)]; + const id = faker.number.int(); + const projectId = faker.number.int(); + + jest + .spyOn(MockRoleService, 'update') + .mockRejectedValue(new RoleAlreadyExistsException()); + + await expect(controller.updateRole(projectId, id, dto)).rejects.toThrow( + RoleAlreadyExistsException, + ); + expect(MockRoleService.update).toHaveBeenCalledTimes(1); + expect(MockRoleService.update).toHaveBeenCalledWith(id, projectId, dto); + }); + + it('should handle service errors properly', async () => { + const dto = new UpdateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [getRandomEnumValue(PermissionEnum)]; + const id = faker.number.int(); + const projectId = faker.number.int(); + const error = new Error('Database error'); + + jest.spyOn(MockRoleService, 'update').mockRejectedValue(error); + + await expect(controller.updateRole(projectId, id, dto)).rejects.toThrow( + 'Database error', + ); + expect(MockRoleService.update).toHaveBeenCalledTimes(1); + expect(MockRoleService.update).toHaveBeenCalledWith(id, projectId, dto); + }); }); - it('deleteRole', async () => { - const id = faker.number.int(); + describe('deleteRole', () => { + it('should delete a role successfully', async () => { + const id = faker.number.int(); + + jest.spyOn(MockRoleService, 'deleteById').mockResolvedValue(undefined); - await controller.deleteRole(id); + await controller.deleteRole(id); - expect(MockRoleService.deleteById).toHaveBeenCalledTimes(1); - expect(MockRoleService.deleteById).toHaveBeenCalledWith(id); + expect(MockRoleService.deleteById).toHaveBeenCalledTimes(1); + expect(MockRoleService.deleteById).toHaveBeenCalledWith(id); + }); + + it('should throw RoleNotFoundException when role does not exist', async () => { + const id = faker.number.int(); + + jest + .spyOn(MockRoleService, 'deleteById') + .mockRejectedValue(new RoleNotFoundException()); + + await expect(controller.deleteRole(id)).rejects.toThrow( + RoleNotFoundException, + ); + expect(MockRoleService.deleteById).toHaveBeenCalledTimes(1); + expect(MockRoleService.deleteById).toHaveBeenCalledWith(id); + }); + + it('should handle service errors properly', async () => { + const id = faker.number.int(); + const error = new Error('Database error'); + + jest.spyOn(MockRoleService, 'deleteById').mockRejectedValue(error); + + await expect(controller.deleteRole(id)).rejects.toThrow('Database error'); + expect(MockRoleService.deleteById).toHaveBeenCalledTimes(1); + expect(MockRoleService.deleteById).toHaveBeenCalledWith(id); + }); + + it('should handle foreign key constraint errors', async () => { + const id = faker.number.int(); + const error = new Error('Foreign key constraint violation'); + + jest.spyOn(MockRoleService, 'deleteById').mockRejectedValue(error); + + await expect(controller.deleteRole(id)).rejects.toThrow( + 'Foreign key constraint violation', + ); + expect(MockRoleService.deleteById).toHaveBeenCalledTimes(1); + expect(MockRoleService.deleteById).toHaveBeenCalledWith(id); + }); }); }); diff --git a/apps/api/src/domains/admin/project/role/role.service.spec.ts b/apps/api/src/domains/admin/project/role/role.service.spec.ts index a0c72eed9..3b2c38486 100644 --- a/apps/api/src/domains/admin/project/role/role.service.spec.ts +++ b/apps/api/src/domains/admin/project/role/role.service.spec.ts @@ -50,13 +50,24 @@ describe('RoleService', () => { dto.permissions = getRandomEnumValues(PermissionEnum); dto.projectId = faker.number.int(); jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + ...dto, + id: faker.number.int(), + project: { id: dto.projectId }, + members: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + } as unknown as RoleEntity); const role = await roleService.create(dto); expect(role.name).toEqual(dto.name); expect(role.permissions).toEqual(dto.permissions); expect(role.project.id).toEqual(dto.projectId); + expect(roleRepo.save).toHaveBeenCalledTimes(1); }); + it('creating a role fails with duplicate inputs', async () => { const dto = new CreateRoleDto(); dto.name = faker.string.sample(); @@ -65,6 +76,7 @@ describe('RoleService', () => { jest .spyOn(roleRepo, 'findOneBy') .mockResolvedValue({ id: faker.number.int() } as RoleEntity); + jest.spyOn(roleRepo, 'save'); await expect(roleService.create(dto)).rejects.toThrow( RoleAlreadyExistsException, @@ -75,6 +87,66 @@ describe('RoleService', () => { name: dto.name, project: { id: dto.projectId }, }); + expect(roleRepo.save).not.toHaveBeenCalled(); + }); + + it('creating a role validates role name uniqueness within project', async () => { + const projectId = faker.number.int(); + const roleName = faker.string.sample(); + + const dto1 = new CreateRoleDto(); + dto1.name = roleName; + dto1.permissions = getRandomEnumValues(PermissionEnum); + dto1.projectId = projectId; + + const dto2 = new CreateRoleDto(); + dto2.name = roleName; + dto2.permissions = getRandomEnumValues(PermissionEnum); + dto2.projectId = projectId; + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValueOnce(null); + jest.spyOn(roleRepo, 'save').mockResolvedValueOnce({ + ...dto1, + id: faker.number.int(), + members: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + } as unknown as RoleEntity); + + await roleService.create(dto1); + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValueOnce({ + id: faker.number.int(), + } as unknown as RoleEntity); + + await expect(roleService.create(dto2)).rejects.toThrow( + RoleAlreadyExistsException, + ); + }); + + it('creating a role allows same name in different projects', async () => { + const roleName = faker.string.sample(); + + const dto1 = new CreateRoleDto(); + dto1.name = roleName; + dto1.permissions = getRandomEnumValues(PermissionEnum); + dto1.projectId = faker.number.int(); + + const dto2 = new CreateRoleDto(); + dto2.name = roleName; + dto2.permissions = getRandomEnumValues(PermissionEnum); + dto2.projectId = faker.number.int(); + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + id: faker.number.int(), + } as unknown as RoleEntity); + + await roleService.create(dto1); + await roleService.create(dto2); + + expect(roleRepo.save).toHaveBeenCalledTimes(2); }); }); @@ -93,15 +165,66 @@ describe('RoleService', () => { it('creating roles succeeds with valid inputs', async () => { jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest + .spyOn(roleRepo, 'save') + .mockImplementation((entities: any) => + Promise.resolve(entities as any), + ); - const roles = await roleService.createMany(dtos); + const result = await roleService.createMany(dtos); - expect(roles).toHaveLength(roleCount); + expect(result).toHaveLength(roleCount); + expect(roleRepo.findOneBy).toHaveBeenCalledTimes(roleCount); + expect(roleRepo.save).toHaveBeenCalledTimes(1); }); + it('creating roles fails with duplicate inputs', async () => { + jest + .spyOn(roleRepo, 'findOneBy') + .mockResolvedValue({ id: faker.number.int() } as RoleEntity); + await expect(roleService.createMany(dtos)).rejects.toThrow( RoleAlreadyExistsException, ); + + expect(roleRepo.findOneBy).toHaveBeenCalledTimes(1); + }); + + it('creating roles handles duplicate permissions correctly', async () => { + const dtoWithDuplicatePermissions = new CreateRoleDto(); + dtoWithDuplicatePermissions.name = faker.string.sample(); + dtoWithDuplicatePermissions.permissions = [ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + ]; + dtoWithDuplicatePermissions.projectId = faker.number.int(); + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + ...dtoWithDuplicatePermissions, + id: faker.number.int(), + members: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + } as unknown as RoleEntity); + + const result = await roleService.createMany([ + dtoWithDuplicatePermissions, + ]); + + expect(result).toBeDefined(); + expect(roleRepo.save).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + permissions: expect.arrayContaining([ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + ]), + }), + ]), + ); }); }); @@ -112,13 +235,27 @@ describe('RoleService', () => { const dto = new UpdateRoleDto(); dto.name = faker.string.sample(); dto.permissions = getRandomEnumValues(PermissionEnum); + + const existingRole = { + id: roleId, + name: 'old-name', + permissions: [], + } as unknown as RoleEntity; + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(existingRole); jest.spyOn(roleRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + ...existingRole, + ...dto, + } as unknown as RoleEntity); const role = await roleService.update(roleId, projectId, dto); expect(role.name).toEqual(dto.name); expect(role.permissions).toEqual(dto.permissions); + expect(roleRepo.findOneBy).toHaveBeenCalledWith({ id: roleId }); + expect(roleRepo.save).toHaveBeenCalledTimes(1); }); + it('updating a role fails with a duplicate name', async () => { const roleId = faker.number.int(); const projectId = faker.number.int(); @@ -126,9 +263,90 @@ describe('RoleService', () => { dto.name = faker.string.sample(); dto.permissions = getRandomEnumValues(PermissionEnum); + const existingRole = { + id: roleId, + name: 'old-name', + permissions: [], + } as unknown as RoleEntity; + const duplicateRole = { + id: faker.number.int(), + name: dto.name, + } as unknown as RoleEntity; + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(existingRole); + jest.spyOn(roleRepo, 'findOne').mockResolvedValue(duplicateRole); + await expect(roleService.update(roleId, projectId, dto)).rejects.toThrow( RoleAlreadyExistsException, ); + + expect(roleRepo.findOne).toHaveBeenCalledWith({ + where: { + name: dto.name, + project: { id: projectId }, + id: expect.any(Object), + }, + }); + }); + + it('updating a role creates new role when id does not exist', async () => { + const roleId = faker.number.int(); + const projectId = faker.number.int(); + const dto = new UpdateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = getRandomEnumValues(PermissionEnum); + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'findOne').mockResolvedValue(null); + jest + .spyOn(roleRepo, 'save') + .mockResolvedValue({ id: roleId, ...dto } as unknown as RoleEntity); + + const role = await roleService.update(roleId, projectId, dto); + + expect(role.name).toEqual(dto.name); + expect(role.permissions).toEqual(dto.permissions); + expect(roleRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + name: dto.name, + permissions: dto.permissions, + }), + ); + }); + + it('updating a role removes duplicate permissions', async () => { + const roleId = faker.number.int(); + const projectId = faker.number.int(); + const dto = new UpdateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = [ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + ]; + + const existingRole = { + id: roleId, + name: 'old-name', + permissions: [], + } as unknown as RoleEntity; + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(existingRole); + jest.spyOn(roleRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + ...existingRole, + ...dto, + } as unknown as RoleEntity); + + await roleService.update(roleId, projectId, dto); + + expect(roleRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + permissions: [ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + ], + }), + ); }); }); @@ -173,4 +391,310 @@ describe('RoleService', () => { ).rejects.toThrow(RoleNotFoundException); }); }); + + describe('findByUserId', () => { + it('finding roles by user id succeeds with valid user id', async () => { + const userId = faker.number.int(); + const mockRoles = [ + { + id: faker.number.int(), + name: faker.string.sample(), + project: { id: faker.number.int() }, + }, + { + id: faker.number.int(), + name: faker.string.sample(), + project: { id: faker.number.int() }, + }, + ] as RoleEntity[]; + + jest.spyOn(roleRepo, 'find').mockResolvedValue(mockRoles); + + const result = await roleService.findByUserId(userId); + + expect(result).toEqual(mockRoles); + expect(roleRepo.find).toHaveBeenCalledWith({ + where: { members: { user: { id: userId } } }, + relations: { project: true }, + }); + }); + + it('finding roles by user id returns empty array when user has no roles', async () => { + const userId = faker.number.int(); + jest.spyOn(roleRepo, 'find').mockResolvedValue([]); + + const result = await roleService.findByUserId(userId); + + expect(result).toEqual([]); + expect(roleRepo.find).toHaveBeenCalledWith({ + where: { members: { user: { id: userId } } }, + relations: { project: true }, + }); + }); + }); + + describe('findByProjectId', () => { + it('finding roles by project id succeeds with valid project id', async () => { + const projectId = faker.number.int(); + const mockRoles = [ + { id: faker.number.int(), name: faker.string.sample() }, + { id: faker.number.int(), name: faker.string.sample() }, + ] as RoleEntity[]; + const total = mockRoles.length; + + jest + .spyOn(roleRepo, 'findAndCountBy') + .mockResolvedValue([mockRoles, total]); + + const result = await roleService.findByProjectId(projectId); + + expect(result.roles).toEqual(mockRoles); + expect(result.total).toEqual(total); + expect(roleRepo.findAndCountBy).toHaveBeenCalledWith({ + project: { id: projectId }, + }); + }); + + it('finding roles by project id returns empty result when project has no roles', async () => { + const projectId = faker.number.int(); + jest.spyOn(roleRepo, 'findAndCountBy').mockResolvedValue([[], 0]); + + const result = await roleService.findByProjectId(projectId); + + expect(result.roles).toEqual([]); + expect(result.total).toEqual(0); + }); + }); + + describe('findAndCount', () => { + it('finding all roles succeeds', async () => { + const mockRoles = [ + { id: faker.number.int(), name: faker.string.sample() }, + { id: faker.number.int(), name: faker.string.sample() }, + { id: faker.number.int(), name: faker.string.sample() }, + ] as RoleEntity[]; + const total = mockRoles.length; + + jest + .spyOn(roleRepo, 'findAndCount') + .mockResolvedValue([mockRoles, total]); + + const result = await roleService.findAndCount(); + + expect(result.roles).toEqual(mockRoles); + expect(result.total).toEqual(total); + expect(roleRepo.findAndCount).toHaveBeenCalledTimes(1); + }); + + it('finding all roles returns empty result when no roles exist', async () => { + jest.spyOn(roleRepo, 'findAndCount').mockResolvedValue([[], 0]); + + const result = await roleService.findAndCount(); + + expect(result.roles).toEqual([]); + expect(result.total).toEqual(0); + }); + }); + + describe('deleteById', () => { + it('deleting a role succeeds with valid role id', async () => { + const roleId = faker.number.int(); + jest.spyOn(roleRepo, 'remove').mockResolvedValue({} as RoleEntity); + + await roleService.deleteById(roleId); + + expect(roleRepo.remove).toHaveBeenCalledWith( + expect.objectContaining({ id: roleId }), + ); + }); + + it('deleting a role handles non-existent role gracefully', async () => { + const roleId = faker.number.int(); + jest.spyOn(roleRepo, 'remove').mockResolvedValue({} as RoleEntity); + + await expect(roleService.deleteById(roleId)).resolves.not.toThrow(); + + expect(roleRepo.remove).toHaveBeenCalledWith( + expect.objectContaining({ id: roleId }), + ); + }); + }); + + describe('validateRoleName (private method)', () => { + it('validateRoleName throws exception when role exists', async () => { + const name = faker.string.sample(); + const projectId = faker.number.int(); + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue({ + id: faker.number.int(), + } as unknown as RoleEntity); + + const dto = new CreateRoleDto(); + dto.name = name; + dto.permissions = getRandomEnumValues(PermissionEnum); + dto.projectId = projectId; + + await expect(roleService.create(dto)).rejects.toThrow( + RoleAlreadyExistsException, + ); + + expect(roleRepo.findOneBy).toHaveBeenCalledWith({ + name, + project: { id: projectId }, + }); + }); + + it('validateRoleName passes when role does not exist', async () => { + const name = faker.string.sample(); + const projectId = faker.number.int(); + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + id: faker.number.int(), + name, + project: { id: projectId }, + } as unknown as RoleEntity); + + const dto = new CreateRoleDto(); + dto.name = name; + dto.permissions = getRandomEnumValues(PermissionEnum); + dto.projectId = projectId; + + await roleService.create(dto); + + expect(roleRepo.findOneBy).toHaveBeenCalledWith({ + name, + project: { id: projectId }, + }); + expect(roleRepo.save).toHaveBeenCalledTimes(1); + }); + }); + + describe('integration scenarios', () => { + it('handles complex role management workflow', async () => { + const projectId = faker.number.int(); + const _userId = faker.number.int(); + + const roles = [ + { + name: 'admin', + permissions: [ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + ], + }, + { name: 'user', permissions: [PermissionEnum.feedback_download_read] }, + { name: 'guest', permissions: [] }, + ]; + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + id: faker.number.int(), + project: { id: projectId }, + members: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + } as unknown as RoleEntity); + + for (const roleData of roles) { + const dto = new CreateRoleDto(); + dto.name = roleData.name; + dto.permissions = roleData.permissions; + dto.projectId = projectId; + + await roleService.create(dto); + } + + const mockRoles = roles.map((r) => ({ + ...r, + id: faker.number.int(), + })) as RoleEntity[]; + jest + .spyOn(roleRepo, 'findAndCountBy') + .mockResolvedValue([mockRoles, mockRoles.length]); + + const projectRoles = await roleService.findByProjectId(projectId); + expect(projectRoles.roles).toHaveLength(3); + expect(projectRoles.total).toBe(3); + + const updateDto = new UpdateRoleDto(); + updateDto.name = 'updated-admin'; + updateDto.permissions = [ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + PermissionEnum.feedback_delete, + ]; + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(mockRoles[0]); + jest.spyOn(roleRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + ...mockRoles[0], + ...updateDto, + } as unknown as RoleEntity); + + const updatedRole = await roleService.update( + mockRoles[0].id, + projectId, + updateDto, + ); + expect(updatedRole.name).toBe('updated-admin'); + expect(updatedRole.permissions).toEqual([ + PermissionEnum.feedback_download_read, + PermissionEnum.feedback_update, + PermissionEnum.feedback_delete, + ]); + + jest.spyOn(roleRepo, 'remove').mockResolvedValue({} as RoleEntity); + await roleService.deleteById(mockRoles[0].id); + expect(roleRepo.remove).toHaveBeenCalledWith( + expect.objectContaining({ id: mockRoles[0].id }), + ); + }); + + it('handles edge cases with empty permissions', async () => { + const dto = new CreateRoleDto(); + dto.name = faker.string.sample(); + dto.permissions = []; + dto.projectId = faker.number.int(); + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + ...dto, + id: faker.number.int(), + members: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + } as unknown as RoleEntity); + + const role = await roleService.create(dto); + expect(role.permissions).toEqual([]); + }); + + it('handles edge cases with null/undefined values gracefully', async () => { + const roleId = faker.number.int(); + const projectId = faker.number.int(); + + const dto = new UpdateRoleDto(); + dto.name = null as any; + dto.permissions = null as any; + + jest.spyOn(roleRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(roleRepo, 'findOne').mockResolvedValue(null); + jest.spyOn(roleRepo, 'save').mockResolvedValue({ + id: roleId, + name: null, + permissions: null, + members: [], + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + } as unknown as RoleEntity); + + const result = await roleService.update(roleId, projectId, dto); + expect(result.name).toBeNull(); + expect(result.permissions).toBeNull(); + }); + }); }); diff --git a/apps/api/src/domains/admin/project/webhook/dtos/requests/create-webhook-request.dto.ts b/apps/api/src/domains/admin/project/webhook/dtos/requests/create-webhook-request.dto.ts index d6b943104..c0350f97b 100644 --- a/apps/api/src/domains/admin/project/webhook/dtos/requests/create-webhook-request.dto.ts +++ b/apps/api/src/domains/admin/project/webhook/dtos/requests/create-webhook-request.dto.ts @@ -14,7 +14,7 @@ * under the License. */ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsEnum, IsString } from 'class-validator'; +import { IsArray, IsEnum, IsNotEmpty, IsString, IsUrl } from 'class-validator'; import { WebhookStatusEnum } from '@/common/enums'; import { TokenValidator } from '@/common/validators/token-validator'; @@ -24,10 +24,12 @@ import { EventDto } from '..'; export class CreateWebhookRequestDto { @ApiProperty() @IsString() + @IsNotEmpty() name: string; @ApiProperty() @IsString() + @IsUrl() url: string; @ApiProperty({ enum: WebhookStatusEnum }) @@ -36,6 +38,7 @@ export class CreateWebhookRequestDto { @ApiProperty({ type: [EventDto] }) @IsArray() + @IsNotEmpty() events: EventDto[]; @ApiProperty({ nullable: true, type: String }) diff --git a/apps/api/src/domains/admin/project/webhook/exceptions/index.ts b/apps/api/src/domains/admin/project/webhook/exceptions/index.ts index ef7c309b6..c0c77304a 100644 --- a/apps/api/src/domains/admin/project/webhook/exceptions/index.ts +++ b/apps/api/src/domains/admin/project/webhook/exceptions/index.ts @@ -14,3 +14,4 @@ * under the License. */ export { WebhookAlreadyExistsException } from './webhook-already-exists.exception'; +export { WebhookNotFoundException } from './webhook-not-found.exception'; diff --git a/apps/api/src/domains/admin/project/webhook/exceptions/webhook-not-found.exception.ts b/apps/api/src/domains/admin/project/webhook/exceptions/webhook-not-found.exception.ts new file mode 100644 index 000000000..d8a5a5fc4 --- /dev/null +++ b/apps/api/src/domains/admin/project/webhook/exceptions/webhook-not-found.exception.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { NotFoundException } from '@nestjs/common'; + +import { ErrorCode } from '@ufb/shared'; + +export class WebhookNotFoundException extends NotFoundException { + constructor() { + super({ + code: ErrorCode.Webhook.WebhookNotFound, + message: 'Webhook not found', + }); + } +} diff --git a/apps/api/src/domains/admin/project/webhook/webhook.listener.spec.ts b/apps/api/src/domains/admin/project/webhook/webhook.listener.spec.ts index 450828b44..5fb537f32 100644 --- a/apps/api/src/domains/admin/project/webhook/webhook.listener.spec.ts +++ b/apps/api/src/domains/admin/project/webhook/webhook.listener.spec.ts @@ -14,11 +14,14 @@ * under the License. */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + import { faker } from '@faker-js/faker'; import { HttpService } from '@nestjs/axios'; +import { NotFoundException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import type { AxiosResponse } from 'axios'; -import { of } from 'rxjs'; +import type { AxiosError, AxiosResponse } from 'axios'; +import { of, throwError } from 'rxjs'; import { EventTypeEnum, IssueStatusEnum } from '@/common/enums'; import { webhookFixture } from '@/test-utils/fixtures'; @@ -29,6 +32,7 @@ import { WebhookListener } from './webhook.listener'; describe('webhook listener', () => { let webhookListener: WebhookListener; let httpService: HttpService; + let webhookListenerAny: any; beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestConfig], @@ -36,6 +40,7 @@ describe('webhook listener', () => { }).compile(); webhookListener = module.get(WebhookListener); httpService = module.get(HttpService); + webhookListenerAny = webhookListener as any; }); describe('handleFeedbackCreation', () => { @@ -64,6 +69,82 @@ describe('webhook listener', () => { }, ); }); + + it('throws NotFoundException when feedback is not found', async () => { + // Mock repository to return null + const feedbackRepo = webhookListenerAny.feedbackRepo; + + jest.spyOn(feedbackRepo, 'findOne').mockResolvedValue(null); + + await expect( + webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }), + ).rejects.toThrow(NotFoundException); + }); + + it('handles HTTP errors gracefully when sending webhooks', async () => { + const axiosError: AxiosError = { + name: 'AxiosError', + message: 'Request failed', + code: 'ECONNREFUSED', + response: { + data: { error: 'Connection refused' }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: {} as any, + }, + config: {} as any, + isAxiosError: true, + toJSON: () => ({}), + }; + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => throwError(() => axiosError)); + + // Should not throw error, but handle gracefully + await expect( + webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }), + ).resolves.not.toThrow(); + }); + + it('validates webhook data structure when feedback is created', async () => { + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + await webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }); + + expect(httpService.post).toHaveBeenCalledWith( + webhookFixture.url, + expect.objectContaining({ + event: EventTypeEnum.FEEDBACK_CREATION, + data: expect.objectContaining({ + feedback: expect.objectContaining({ + id: expect.any(Number), + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + issues: expect.any(Array), + }), + channel: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + }), + project: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + }), + }), + }), + expect.any(Object), + ); + }); }); describe('handleIssueAddition', () => { @@ -93,6 +174,56 @@ describe('webhook listener', () => { }, ); }); + + it('handles gracefully when feedback is not found for issue addition', async () => { + const feedbackRepo = webhookListenerAny.feedbackRepo; + jest.spyOn(feedbackRepo, 'findOne').mockResolvedValue(null); + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + // Should throw error because feedback.channel.project.id is accessed + await expect( + webhookListener.handleIssueAddition({ + feedbackId: faker.number.int(), + issueId: faker.number.int(), + }), + ).rejects.toThrow(); + }); + + it('validates webhook data structure when issue is added', async () => { + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + const issueId = faker.number.int(); + await webhookListener.handleIssueAddition({ + feedbackId: faker.number.int(), + issueId, + }); + + expect(httpService.post).toHaveBeenCalledWith( + webhookFixture.url, + expect.objectContaining({ + event: EventTypeEnum.ISSUE_ADDITION, + data: expect.objectContaining({ + feedback: expect.objectContaining({ + id: expect.any(Number), + }), + channel: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + }), + project: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + }), + }), + }), + expect.any(Object), + ); + }); }); describe('handleIssueCreation', () => { @@ -121,6 +252,53 @@ describe('webhook listener', () => { }, ); }); + + it('handles gracefully when issue is not found for creation', async () => { + const issueRepo = webhookListenerAny.issueRepo; + jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + // Should throw error because issue.project.id is accessed + await expect( + webhookListener.handleIssueCreation({ + issueId: faker.number.int(), + }), + ).rejects.toThrow(); + }); + + it('validates webhook data structure when issue is created', async () => { + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + await webhookListener.handleIssueCreation({ + issueId: faker.number.int(), + }); + + expect(httpService.post).toHaveBeenCalledWith( + webhookFixture.url, + expect.objectContaining({ + event: EventTypeEnum.ISSUE_CREATION, + data: expect.objectContaining({ + issue: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + description: expect.any(String), + status: expect.any(String), + feedbackCount: expect.any(Number), + }), + project: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + }), + }), + }), + expect.any(Object), + ); + }); }); describe('handleIssueStatusChange', () => { @@ -150,5 +328,260 @@ describe('webhook listener', () => { }, ); }); + + it('handles gracefully when issue is not found for status change', async () => { + const issueRepo = webhookListenerAny.issueRepo; + jest.spyOn(issueRepo, 'findOne').mockResolvedValue(null); + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + // Should throw error because issue.project.id is accessed + await expect( + webhookListener.handleIssueStatusChange({ + issueId: faker.number.int(), + previousStatus: getRandomEnumValue(IssueStatusEnum), + }), + ).rejects.toThrow(); + }); + + it('validates webhook data structure when issue status is changed', async () => { + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + const previousStatus = getRandomEnumValue(IssueStatusEnum); + await webhookListener.handleIssueStatusChange({ + issueId: faker.number.int(), + previousStatus, + }); + + expect(httpService.post).toHaveBeenCalledWith( + webhookFixture.url, + expect.objectContaining({ + event: EventTypeEnum.ISSUE_STATUS_CHANGE, + data: expect.objectContaining({ + issue: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + description: expect.any(String), + status: expect.any(String), + feedbackCount: expect.any(Number), + }), + project: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + }), + previousStatus, + }), + }), + expect.any(Object), + ); + }); + }); + + describe('edge cases and error scenarios', () => { + it('handles empty webhook list gracefully', async () => { + const webhookRepo = webhookListenerAny.webhookRepo; + jest.spyOn(webhookRepo, 'find').mockResolvedValue([]); + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + // Should not throw error when no webhooks are found + await expect( + webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }), + ).resolves.not.toThrow(); + + expect(httpService.post).not.toHaveBeenCalled(); + }); + + it('handles inactive webhooks correctly', async () => { + const webhookRepo = webhookListenerAny.webhookRepo; + // Mock to return empty array for inactive webhooks + jest.spyOn(webhookRepo, 'find').mockResolvedValue([]); + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + await webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }); + + // Should not send webhooks for inactive webhooks + expect(httpService.post).not.toHaveBeenCalled(); + }); + + it('handles inactive events correctly', async () => { + const webhookRepo = webhookListenerAny.webhookRepo; + // Mock to return empty array for inactive events + jest.spyOn(webhookRepo, 'find').mockResolvedValue([]); + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + await webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }); + + // Should not send webhooks for inactive events + expect(httpService.post).not.toHaveBeenCalled(); + }); + + it('handles network timeout errors', async () => { + const timeoutError: AxiosError = { + name: 'AxiosError', + message: 'timeout of 5000ms exceeded', + code: 'ECONNABORTED', + response: undefined, + config: {} as any, + isAxiosError: true, + toJSON: () => ({}), + }; + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => throwError(() => timeoutError)); + + // Should handle timeout gracefully + await expect( + webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }), + ).resolves.not.toThrow(); + }); + + it('handles malformed response errors', async () => { + const malformedError: AxiosError = { + name: 'AxiosError', + message: 'Request failed with status code 400', + code: 'ERR_BAD_REQUEST', + response: { + data: 'Invalid JSON response', + status: 400, + statusText: 'Bad Request', + headers: {}, + config: {} as any, + }, + config: {} as any, + isAxiosError: true, + toJSON: () => ({}), + }; + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => throwError(() => malformedError)); + + // Should handle malformed response gracefully + await expect( + webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }), + ).resolves.not.toThrow(); + }); + }); + + describe('retry logic and logging', () => { + it('handles retry logic for failed requests', async () => { + let callCount = 0; + const axiosError: AxiosError = { + name: 'AxiosError', + message: 'Request failed', + code: 'ECONNREFUSED', + response: undefined, + config: {} as any, + isAxiosError: true, + toJSON: () => ({}), + }; + + jest.spyOn(httpService, 'post').mockImplementation(() => { + callCount++; + // Always return error to test retry behavior + return throwError(() => axiosError); + }); + + await webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }); + + // Should make at least one call (retry behavior may vary in test environment) + expect(callCount).toBeGreaterThan(0); + }); + + it('logs successful webhook sends', async () => { + const loggerSpy = jest.spyOn(webhookListenerAny.logger, 'log'); + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => of({} as AxiosResponse)); + + await webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }); + + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringContaining('Successfully sent webhook to'), + ); + }); + + it('logs webhook retry attempts', async () => { + const loggerSpy = jest.spyOn(webhookListenerAny.logger, 'warn'); + const axiosError: AxiosError = { + name: 'AxiosError', + message: 'Request failed', + code: 'ECONNREFUSED', + response: undefined, + config: {} as any, + isAxiosError: true, + toJSON: () => ({}), + }; + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => throwError(() => axiosError)); + + await webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }); + + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringContaining('Retrying webhook... Attempt #'), + ); + }); + + it('handles webhook errors gracefully', async () => { + const axiosError: AxiosError = { + name: 'AxiosError', + message: 'Request failed', + code: 'ECONNREFUSED', + response: { + data: { error: 'Connection refused' }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: {} as any, + }, + config: {} as any, + isAxiosError: true, + toJSON: () => ({}), + }; + + jest + .spyOn(httpService, 'post') + .mockImplementation(() => throwError(() => axiosError)); + + // Should handle errors gracefully without throwing + await expect( + webhookListener.handleFeedbackCreation({ + feedbackId: faker.number.int(), + }), + ).resolves.not.toThrow(); + }); }); }); diff --git a/apps/api/src/domains/admin/project/webhook/webhook.service.spec.ts b/apps/api/src/domains/admin/project/webhook/webhook.service.spec.ts index d515fb47f..ca9cf1a2f 100644 --- a/apps/api/src/domains/admin/project/webhook/webhook.service.spec.ts +++ b/apps/api/src/domains/admin/project/webhook/webhook.service.spec.ts @@ -30,7 +30,10 @@ import { getRandomEnumValue, TestConfig } from '@/test-utils/util-functions'; import { WebhookServiceProviders } from '../../../../test-utils/providers/webhook.service.provider'; import { ChannelEntity } from '../../channel/channel/channel.entity'; import type { CreateWebhookDto, UpdateWebhookDto } from './dtos'; -import { WebhookAlreadyExistsException } from './exceptions'; +import { + WebhookAlreadyExistsException, + WebhookNotFoundException, +} from './exceptions'; import { WebhookEntity } from './webhook.entity'; import { WebhookService } from './webhook.service'; @@ -381,4 +384,303 @@ describe('webhook service', () => { }); }); }); + + describe('findById', () => { + it('should return webhook with events and channels when webhook exists', async () => { + const webhookId = webhookFixture.id; + jest.spyOn(webhookRepo, 'find').mockResolvedValue([webhookFixture]); + + const result = await webhookService.findById(webhookId); + + expect(result).toEqual([webhookFixture]); + expect(webhookRepo.find).toHaveBeenCalledWith({ + where: { id: webhookId }, + relations: { events: { channels: true } }, + }); + }); + + it('should return empty array when webhook does not exist', async () => { + const webhookId = faker.number.int(); + jest.spyOn(webhookRepo, 'find').mockResolvedValue([]); + + const result = await webhookService.findById(webhookId); + + expect(result).toEqual([]); + expect(webhookRepo.find).toHaveBeenCalledWith({ + where: { id: webhookId }, + relations: { events: { channels: true } }, + }); + }); + }); + + describe('findByProjectId', () => { + it('should return webhooks for given project when webhooks exist', async () => { + const projectId = webhookFixture.project.id; + const webhooks = [webhookFixture]; + jest.spyOn(webhookRepo, 'find').mockResolvedValue(webhooks); + + const result = await webhookService.findByProjectId(projectId); + + expect(result).toEqual(webhooks); + expect(webhookRepo.find).toHaveBeenCalledWith({ + where: { project: { id: projectId } }, + relations: { events: { channels: true } }, + }); + }); + + it('should return empty array when no webhooks exist for project', async () => { + const projectId = faker.number.int(); + jest.spyOn(webhookRepo, 'find').mockResolvedValue([]); + + const result = await webhookService.findByProjectId(projectId); + + expect(result).toEqual([]); + expect(webhookRepo.find).toHaveBeenCalledWith({ + where: { project: { id: projectId } }, + relations: { events: { channels: true } }, + }); + }); + }); + + describe('delete', () => { + it('should delete webhook when webhook exists', async () => { + const webhookId = webhookFixture.id; + jest.spyOn(webhookRepo, 'findOne').mockResolvedValue(webhookFixture); + jest.spyOn(webhookRepo, 'remove').mockResolvedValue(webhookFixture); + + await webhookService.delete(webhookId); + + expect(webhookRepo.findOne).toHaveBeenCalledWith({ + where: { id: webhookId }, + }); + expect(webhookRepo.remove).toHaveBeenCalledWith(webhookFixture); + }); + + it('should throw WebhookNotFoundException when webhook does not exist', async () => { + const webhookId = faker.number.int(); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValue(null); + + await expect(webhookService.delete(webhookId)).rejects.toThrow( + new WebhookNotFoundException(), + ); + + expect(webhookRepo.findOne).toHaveBeenCalledWith({ + where: { id: webhookId }, + }); + }); + }); + + describe('validateEvent', () => { + describe('events requiring channel IDs', () => { + it('should return true when FEEDBACK_CREATION has valid channel IDs', async () => { + const channelIds = [faker.number.int(), faker.number.int()]; + jest + .spyOn(channelRepo, 'findBy') + .mockResolvedValue( + channelIds.map((id) => ({ id })) as ChannelEntity[], + ); + + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.FEEDBACK_CREATION, + channelIds, + }); + + expect(result).toBe(true); + expect(channelRepo.findBy).toHaveBeenCalledWith({ + id: expect.objectContaining({ _type: 'in', _value: channelIds }), + }); + }); + + it('should return false when FEEDBACK_CREATION has invalid channel IDs', async () => { + const channelIds = [faker.number.int(), faker.number.int()]; + jest + .spyOn(channelRepo, 'findBy') + .mockResolvedValue([{ id: channelIds[0] }] as ChannelEntity[]); + + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.FEEDBACK_CREATION, + channelIds, + }); + + expect(result).toBe(false); + }); + + it('should return true when ISSUE_ADDITION has valid channel IDs', async () => { + const channelIds = [faker.number.int()]; + jest + .spyOn(channelRepo, 'findBy') + .mockResolvedValue( + channelIds.map((id) => ({ id })) as ChannelEntity[], + ); + + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.ISSUE_ADDITION, + channelIds, + }); + + expect(result).toBe(true); + }); + + it('should return false when ISSUE_ADDITION has invalid channel IDs', async () => { + const channelIds = [faker.number.int()]; + jest.spyOn(channelRepo, 'findBy').mockResolvedValue([]); + + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.ISSUE_ADDITION, + channelIds, + }); + + expect(result).toBe(false); + }); + }); + + describe('events excluding channel IDs', () => { + it('should return true when ISSUE_CREATION has empty channel IDs', async () => { + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.ISSUE_CREATION, + channelIds: [], + }); + + expect(result).toBe(true); + }); + + it('should return false when ISSUE_CREATION has non-empty channel IDs', async () => { + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.ISSUE_CREATION, + channelIds: [faker.number.int()], + }); + + expect(result).toBe(false); + }); + + it('should return true when ISSUE_STATUS_CHANGE has empty channel IDs', async () => { + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.ISSUE_STATUS_CHANGE, + channelIds: [], + }); + + expect(result).toBe(true); + }); + + it('should return false when ISSUE_STATUS_CHANGE has non-empty channel IDs', async () => { + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.ISSUE_STATUS_CHANGE, + channelIds: [faker.number.int()], + }); + + expect(result).toBe(false); + }); + }); + + it('should return false for unknown event type', async () => { + const result = await webhookService.validateEvent({ + status: EventStatusEnum.ACTIVE, + type: 'UNKNOWN_EVENT_TYPE' as EventTypeEnum, + channelIds: [], + }); + + expect(result).toBe(false); + }); + }); + + describe('create - additional edge cases', () => { + it('should handle empty events array', async () => { + const dto: CreateWebhookDto = createCreateWebhookDto({ + events: [], + }); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValue(null); + + const webhook = await webhookService.create(dto); + + expect(webhook.events).toEqual([]); + }); + + it('should handle null token', async () => { + const dto: CreateWebhookDto = createCreateWebhookDto({ + token: null, + }); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValue(null); + + const webhook = await webhookService.create(dto); + + expect(webhook.token).toBeNull(); + }); + + it('should handle undefined token', async () => { + const dto: CreateWebhookDto = { + projectId: webhookFixture.project.id, + name: faker.string.sample(), + url: faker.internet.url(), + token: undefined as any, // TypeScript 타입 체크를 우회하여 undefined 전달 + status: WebhookStatusEnum.ACTIVE, + events: [ + { + status: EventStatusEnum.ACTIVE, + type: EventTypeEnum.FEEDBACK_CREATION, + channelIds: [faker.number.int()], + }, + ], + }; + jest.spyOn(webhookRepo, 'findOne').mockResolvedValue(null); + + const webhook = await webhookService.create(dto); + + // undefined가 실제로 어떻게 처리되는지 확인 + expect(webhook.token).toBeDefined(); + expect(typeof webhook.token).toBe('string'); + }); + }); + + describe('update - additional edge cases', () => { + it('should throw WebhookNotFoundException when updating non-existent webhook', async () => { + const dto: UpdateWebhookDto = createUpdateWebhookDto({ + id: faker.number.int(), + }); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(webhookRepo, 'save').mockResolvedValue(webhookFixture); + + await expect(webhookService.update(dto)).rejects.toThrow( + new WebhookNotFoundException(), + ); + }); + + it('should handle updating webhook with same name but different ID', async () => { + const dto: UpdateWebhookDto = createUpdateWebhookDto({ + name: webhookFixture.name, + id: faker.number.int(), + }); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(webhookFixture); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(webhookRepo, 'save').mockResolvedValue(webhookFixture); + + const webhook = await webhookService.update(dto); + + expect(webhook).toBeDefined(); + }); + + it('should handle updating webhook with null token', async () => { + const dto: UpdateWebhookDto = createUpdateWebhookDto({ + token: null, + }); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(webhookFixture); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(webhookRepo, 'findOne').mockResolvedValueOnce(null); + jest.spyOn(webhookRepo, 'save').mockResolvedValue(webhookFixture); + + const webhook = await webhookService.update(dto); + + expect(webhook.token).toBeNull(); + }); + }); }); diff --git a/apps/api/src/domains/admin/project/webhook/webhook.service.ts b/apps/api/src/domains/admin/project/webhook/webhook.service.ts index 6b30eec86..cdfef28f6 100644 --- a/apps/api/src/domains/admin/project/webhook/webhook.service.ts +++ b/apps/api/src/domains/admin/project/webhook/webhook.service.ts @@ -23,7 +23,10 @@ import { ChannelEntity } from '../../channel/channel/channel.entity'; import type { EventDto } from './dtos'; import { CreateWebhookDto, UpdateWebhookDto } from './dtos'; import { EventEntity } from './event.entity'; -import { WebhookAlreadyExistsException } from './exceptions'; +import { + WebhookAlreadyExistsException, + WebhookNotFoundException, +} from './exceptions'; import { WebhookEntity } from './webhook.entity'; @Injectable() @@ -37,7 +40,7 @@ export class WebhookService { private readonly channelRepo: Repository, ) {} - private async validateEvent(event: EventDto): Promise { + async validateEvent(event: EventDto): Promise { const eventRequiresChannelIds = [ EventTypeEnum.FEEDBACK_CREATION, EventTypeEnum.ISSUE_ADDITION, @@ -116,11 +119,12 @@ export class WebhookService { @Transactional() async update(dto: UpdateWebhookDto): Promise { - const webhook = - (await this.repository.findOne({ - where: { id: dto.id }, - relations: ['events'], - })) ?? new WebhookEntity(); + const webhook = await this.repository.findOne({ + where: { id: dto.id }, + relations: ['events'], + }); + + if (!webhook) throw new WebhookNotFoundException(); if ( await this.repository.findOne({ @@ -159,10 +163,10 @@ export class WebhookService { @Transactional() async delete(webhookId: number) { - const webhook = - (await this.repository.findOne({ - where: { id: webhookId }, - })) ?? new WebhookEntity(); + const webhook = await this.repository.findOne({ + where: { id: webhookId }, + }); + if (!webhook) throw new WebhookNotFoundException(); await this.repository.remove(webhook); } diff --git a/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.controller.spec.ts b/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.controller.spec.ts index 21063ea4e..8c3e75e0a 100644 --- a/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.controller.spec.ts +++ b/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.controller.spec.ts @@ -13,7 +13,6 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { faker } from '@faker-js/faker'; import { Test } from '@nestjs/testing'; import { getMockProvider } from '@/test-utils/util-functions'; @@ -24,7 +23,7 @@ const MockFeedbackIssueStatisticsService = { getCountByDateByIssue: jest.fn(), }; -describe('FeedbackIssue Statistics Controller', () => { +describe('FeedbackIssueStatisticsController', () => { let feedbackIssueStatisticsController: FeedbackIssueStatisticsController; beforeEach(async () => { @@ -44,24 +43,340 @@ describe('FeedbackIssue Statistics Controller', () => { ); }); - it('getCountByDateByIssue', async () => { - jest.spyOn(MockFeedbackIssueStatisticsService, 'getCountByDateByIssue'); - const startDate = '2023-01-01'; - const endDate = '2023-12-01'; - const interval = ['day', 'week', 'month'][ - faker.number.int({ min: 0, max: 2 }) - ] as 'day' | 'week' | 'month'; - const issueIds = [faker.number.int(), faker.number.int()]; - - await feedbackIssueStatisticsController.getCountByDateByIssue( - startDate, - endDate, - interval, - issueIds.join(','), - ); - - expect( - MockFeedbackIssueStatisticsService.getCountByDateByIssue, - ).toHaveBeenCalledTimes(1); + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getCountByDateByIssue', () => { + it('should call service with correct parameters and return transformed response', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const issueIds = '1,2,3'; + const mockServiceResponse = { + issues: [ + { + id: 1, + name: 'Issue 1', + statistics: [ + { + startDate: '2023-01-01', + endDate: '2023-01-01', + feedbackCount: 10, + }, + ], + }, + ], + }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledTimes(1); + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [1, 2, 3], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle empty issueIds string', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'week' as const; + const issueIds = ''; + const mockServiceResponse = { issues: [] }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should filter out invalid issueIds', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'month' as const; + const issueIds = '1,invalid,2,3.5,4'; + const mockServiceResponse = { issues: [] }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [1, 2, 3, 4], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle single issueId', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const issueIds = '42'; + const mockServiceResponse = { + issues: [ + { + id: 42, + name: 'Single Issue', + statistics: [ + { + startDate: '2023-01-01', + endDate: '2023-01-01', + feedbackCount: 5, + }, + ], + }, + ], + }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [42], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle all interval types', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const issueIds = '1,2'; + const mockServiceResponse = { issues: [] }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const intervals: ('day' | 'week' | 'month')[] = ['day', 'week', 'month']; + + for (const interval of intervals) { + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [1, 2], + }); + } + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledTimes(3); + }); + + it('should handle service errors gracefully', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const issueIds = '1,2'; + const error = new Error('Database connection failed'); + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockRejectedValue( + error, + ); + + await expect( + feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ), + ).rejects.toThrow('Database connection failed'); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledTimes(1); + }); + + it('should handle whitespace in issueIds', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const issueIds = ' 1 , 2 , 3 '; + const mockServiceResponse = { issues: [] }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [1, 2, 3], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle negative issueIds', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const issueIds = '1,-2,3'; + const mockServiceResponse = { issues: [] }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [1, -2, 3], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle zero issueIds', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const issueIds = '0'; + const mockServiceResponse = { issues: [] }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [0], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle very large issueIds', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const issueIds = '999999999,1000000000'; + const mockServiceResponse = { issues: [] }; + + MockFeedbackIssueStatisticsService.getCountByDateByIssue.mockResolvedValue( + mockServiceResponse, + ); + + const result = + await feedbackIssueStatisticsController.getCountByDateByIssue( + startDate, + endDate, + interval, + issueIds, + ); + + expect( + MockFeedbackIssueStatisticsService.getCountByDateByIssue, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + issueIds: [999999999, 1000000000], + }); + expect(result).toEqual(mockServiceResponse); + }); }); }); diff --git a/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts b/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts index bcf07d6bb..fb8820e4a 100644 --- a/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts +++ b/apps/api/src/domains/admin/statistics/feedback-issue/feedback-issue-statistics.service.spec.ts @@ -22,7 +22,9 @@ import type { Repository, SelectQueryBuilder } from 'typeorm'; import { FeedbackEntity } from '@/domains/admin/feedback/feedback.entity'; import { IssueEntity } from '@/domains/admin/project/issue/issue.entity'; +import { ProjectNotFoundException } from '@/domains/admin/project/project/exceptions'; import { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { SchedulerLockService } from '@/domains/operation/scheduler-lock/scheduler-lock.service'; import { FeedbackIssueStatisticsServiceProviders } from '@/test-utils/providers/feedback-issue-statistics.service.providers'; import { createQueryBuilder, TestConfig } from '@/test-utils/util-functions'; import { GetCountByDateByIssueDto } from './dtos'; @@ -68,6 +70,56 @@ const feedbackIssueStatsFixture = [ }, ] as FeedbackIssueStatisticsEntity[]; +// Helper function to create realistic test data +const createRealisticProject = ( + overrides: Partial = {}, +): ProjectEntity => + ({ + id: faker.number.int({ min: 1, max: 1000 }), + name: faker.company.name(), + description: faker.lorem.sentence(), + timezone: { + countryCode: faker.location.countryCode(), + name: faker.location.timeZone(), + offset: faker.helpers.arrayElement([ + '+09:00', + '+00:00', + '-08:00', + '-05:00', + ]), + }, + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + ...overrides, + }) as ProjectEntity; + +const createRealisticIssue = ( + overrides: Partial = {}, +): IssueEntity => + ({ + id: faker.number.int({ min: 1, max: 1000 }), + name: faker.lorem.words(3), + description: faker.lorem.sentence(), + status: faker.helpers.arrayElement(['open', 'closed', 'in_progress']), + priority: faker.helpers.arrayElement(['low', 'medium', 'high', 'critical']), + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + ...overrides, + }) as IssueEntity; + +const createRealisticFeedbackIssueStats = ( + overrides: Partial = {}, +): FeedbackIssueStatisticsEntity => + ({ + id: faker.number.int({ min: 1, max: 1000 }), + date: faker.date.past(), + feedbackCount: faker.number.int({ min: 0, max: 100 }), + issue: createRealisticIssue(), + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + ...overrides, + }) as FeedbackIssueStatisticsEntity; + describe('FeedbackIssueStatisticsService suite', () => { let feedbackIssueStatsService: FeedbackIssueStatisticsService; let feedbackIssueStatsRepo: Repository; @@ -75,6 +127,7 @@ describe('FeedbackIssueStatisticsService suite', () => { let issueRepo: Repository; let projectRepo: Repository; let schedulerRegistry: SchedulerRegistry; + let schedulerLockService: SchedulerLockService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -92,6 +145,7 @@ describe('FeedbackIssueStatisticsService suite', () => { issueRepo = module.get(getRepositoryToken(IssueEntity)); projectRepo = module.get(getRepositoryToken(ProjectEntity)); schedulerRegistry = module.get(SchedulerRegistry); + schedulerLockService = module.get(SchedulerLockService); }); describe('getCountByDateByissue', () => { @@ -224,18 +278,149 @@ describe('FeedbackIssueStatisticsService suite', () => { ], }); }); + + it('returns empty result when no statistics found', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-31'; + const interval = 'day'; + const issueIds = [faker.number.int()]; + const dto = new GetCountByDateByIssueDto(); + dto.startDate = startDate; + dto.endDate = endDate; + dto.interval = interval; + dto.issueIds = issueIds; + + jest.spyOn(feedbackIssueStatsRepo, 'find').mockResolvedValue([]); + + const result = await feedbackIssueStatsService.getCountByDateByIssue(dto); + + expect(result).toEqual({ issues: [] }); + }); + + it('handles database error gracefully', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-31'; + const interval = 'day'; + const issueIds = [faker.number.int()]; + const dto = new GetCountByDateByIssueDto(); + dto.startDate = startDate; + dto.endDate = endDate; + dto.interval = interval; + dto.issueIds = issueIds; + + jest + .spyOn(feedbackIssueStatsRepo, 'find') + .mockRejectedValue(new Error('Database error')); + + await expect( + feedbackIssueStatsService.getCountByDateByIssue(dto), + ).rejects.toThrow('Database error'); + }); + + it('handles multiple issues with overlapping statistics', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-01-31'; + const interval = 'day'; + const issueIds = [1, 2]; + + const multipleIssuesFixture = [ + { + id: 1, + date: new Date('2023-01-01'), + feedbackCount: 5, + issue: { id: 1, name: 'issue1' }, + }, + { + id: 2, + date: new Date('2023-01-01'), + feedbackCount: 3, + issue: { id: 2, name: 'issue2' }, + }, + { + id: 3, + date: new Date('2023-01-02'), + feedbackCount: 2, + issue: { id: 1, name: 'issue1' }, + }, + ] as FeedbackIssueStatisticsEntity[]; + + const dto = new GetCountByDateByIssueDto(); + dto.startDate = startDate; + dto.endDate = endDate; + dto.interval = interval; + dto.issueIds = issueIds; + + jest + .spyOn(feedbackIssueStatsRepo, 'find') + .mockResolvedValue(multipleIssuesFixture); + + const result = await feedbackIssueStatsService.getCountByDateByIssue(dto); + + expect(result.issues).toHaveLength(2); + expect(result.issues[0].statistics).toHaveLength(2); + expect(result.issues[1].statistics).toHaveLength(1); + }); + + it('handles large date ranges efficiently', async () => { + const startDate = '2020-01-01'; + const endDate = '2025-12-31'; + const interval = 'month'; + const issueIds = [faker.number.int()]; + + const dto = new GetCountByDateByIssueDto(); + dto.startDate = startDate; + dto.endDate = endDate; + dto.interval = interval; + dto.issueIds = issueIds; + + jest.spyOn(feedbackIssueStatsRepo, 'find').mockResolvedValue([]); + + const result = await feedbackIssueStatsService.getCountByDateByIssue(dto); + + expect(result).toEqual({ issues: [] }); + expect(feedbackIssueStatsRepo.find).toHaveBeenCalledWith({ + where: { + issue: { id: expect.any(Object) }, + date: expect.any(Object), + }, + relations: { issue: true }, + order: { issue: { id: 'ASC' }, date: 'ASC' }, + }); + }); + + it('handles empty issueIds array', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-31'; + const interval = 'day'; + const issueIds: number[] = []; + + const dto = new GetCountByDateByIssueDto(); + dto.startDate = startDate; + dto.endDate = endDate; + dto.interval = interval; + dto.issueIds = issueIds; + + jest.spyOn(feedbackIssueStatsRepo, 'find').mockResolvedValue([]); + + const result = await feedbackIssueStatsService.getCountByDateByIssue(dto); + + expect(result).toEqual({ issues: [] }); + }); }); describe('addCronJobByProjectId', () => { it('adding a cron job succeeds with valid input', async () => { const projectId = faker.number.int(); - jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + const realisticProject = createRealisticProject({ + id: projectId, timezone: { countryCode: 'KR', name: 'Asia/Seoul', offset: '+09:00', }, - } as ProjectEntity); + }); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(realisticProject); jest.spyOn(schedulerRegistry, 'addCronJob'); await feedbackIssueStatsService.addCronJobByProjectId(projectId); @@ -246,6 +431,170 @@ describe('FeedbackIssueStatisticsService suite', () => { expect.anything(), ); }); + + it('throws ProjectNotFoundException when project not found', async () => { + const projectId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + feedbackIssueStatsService.addCronJobByProjectId(projectId), + ).rejects.toThrow(ProjectNotFoundException); + }); + + it('skips adding cron job when job already exists', async () => { + const projectId = faker.number.int(); + const jobName = `feedback-issue-statistics-${projectId}`; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + + const mockCronJobs = new Map(); + mockCronJobs.set(jobName, {}); + jest + .spyOn(schedulerRegistry, 'getCronJobs') + .mockReturnValue(mockCronJobs); + jest.spyOn(schedulerRegistry, 'addCronJob'); + + await feedbackIssueStatsService.addCronJobByProjectId(projectId); + + expect(schedulerRegistry.addCronJob).not.toHaveBeenCalled(); + }); + + it('handles scheduler lock acquisition failure', async () => { + const projectId = faker.number.int(); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + + jest.spyOn(schedulerLockService, 'acquireLock').mockResolvedValue(false); + jest + .spyOn(schedulerLockService, 'releaseLock') + .mockResolvedValue(undefined); + jest.spyOn(schedulerRegistry, 'addCronJob'); + + await feedbackIssueStatsService.addCronJobByProjectId(projectId); + + expect(schedulerRegistry.addCronJob).toHaveBeenCalledTimes(1); + }); + + it('calculates correct cron hour for different timezone offsets', async () => { + const projectId = faker.number.int(); + + // Test with UTC+0 (should run at hour 0) + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'GB', + name: 'Europe/London', + offset: '+00:00', + }, + } as ProjectEntity); + jest.spyOn(schedulerRegistry, 'addCronJob'); + + await feedbackIssueStatsService.addCronJobByProjectId(projectId); + + expect(schedulerRegistry.addCronJob).toHaveBeenCalledTimes(1); + const callArgs = (schedulerRegistry.addCronJob as jest.Mock).mock + .calls[0] as [string, { cronTime: string[] }]; + expect(callArgs[0]).toBe(`feedback-issue-statistics-${projectId}`); + expect(callArgs[1]).toHaveProperty('cronTime'); + }); + + it('handles negative timezone offsets correctly', async () => { + const projectId = faker.number.int(); + + // Test with UTC-8 (should run at hour 8) + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'US', + name: 'America/Los_Angeles', + offset: '-08:00', + }, + } as ProjectEntity); + jest.spyOn(schedulerRegistry, 'addCronJob'); + + await feedbackIssueStatsService.addCronJobByProjectId(projectId); + + expect(schedulerRegistry.addCronJob).toHaveBeenCalledTimes(1); + const callArgs = (schedulerRegistry.addCronJob as jest.Mock).mock + .calls[0] as [string, { cronTime: string[] }]; + expect(callArgs[0]).toBe(`feedback-issue-statistics-${projectId}`); + expect(callArgs[1]).toHaveProperty('cronTime'); + }); + + it('handles scheduler lock service errors gracefully', async () => { + const projectId = faker.number.int(); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + + jest + .spyOn(schedulerLockService, 'acquireLock') + .mockRejectedValue(new Error('Lock service error')); + jest.spyOn(schedulerRegistry, 'addCronJob'); + + // Should not throw error, cron job should still be added + await expect( + feedbackIssueStatsService.addCronJobByProjectId(projectId), + ).resolves.not.toThrow(); + expect(schedulerRegistry.addCronJob).toHaveBeenCalledTimes(1); + }); + + it('handles scheduler registry errors gracefully', async () => { + const projectId = faker.number.int(); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + + jest.spyOn(schedulerRegistry, 'addCronJob').mockImplementation(() => { + throw new Error('Scheduler registry error'); + }); + + await expect( + feedbackIssueStatsService.addCronJobByProjectId(projectId), + ).rejects.toThrow('Scheduler registry error'); + }); + + it('handles extreme timezone offsets', async () => { + const projectId = faker.number.int(); + + // Test with UTC+14 (should run at hour 10, as (24 - 14) % 24 = 10) + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KI', + name: 'Pacific/Kiritimati', + offset: '+14:00', + }, + } as ProjectEntity); + jest.spyOn(schedulerRegistry, 'addCronJob'); + + await feedbackIssueStatsService.addCronJobByProjectId(projectId); + + expect(schedulerRegistry.addCronJob).toHaveBeenCalledTimes(1); + const callArgs = (schedulerRegistry.addCronJob as jest.Mock).mock + .calls[0] as [string, { cronTime: string[] }]; + expect(callArgs[0]).toBe(`feedback-issue-statistics-${projectId}`); + expect(callArgs[1]).toHaveProperty('cronTime'); + }); }); describe('createFeedbackIssueStatistics', () => { @@ -253,17 +602,22 @@ describe('FeedbackIssueStatisticsService suite', () => { const projectId = faker.number.int(); const dayToCreate = faker.number.int({ min: 2, max: 10 }); const issueCount = faker.number.int({ min: 2, max: 10 }); - const issues = Array.from({ length: issueCount }).map(() => ({ - id: faker.number.int(), - })); - jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + + const realisticProject = createRealisticProject({ + id: projectId, timezone: { countryCode: 'KR', name: 'Asia/Seoul', offset: '+09:00', }, - } as ProjectEntity); - jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]); + }); + + const realisticIssues = Array.from({ length: issueCount }).map(() => + createRealisticIssue({ project: { id: projectId } as ProjectEntity }), + ); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(realisticProject); + jest.spyOn(issueRepo, 'find').mockResolvedValue(realisticIssues); jest.spyOn(feedbackRepo, 'count').mockResolvedValueOnce(0); jest.spyOn(feedbackRepo, 'count').mockResolvedValue(1); jest.spyOn(feedbackIssueStatsRepo.manager, 'transaction'); @@ -277,6 +631,214 @@ describe('FeedbackIssueStatisticsService suite', () => { dayToCreate * issueCount, ); }); + + it('throws ProjectNotFoundException when project not found', async () => { + const projectId = faker.number.int(); + const dayToCreate = faker.number.int({ min: 1, max: 5 }); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ), + ).rejects.toThrow(ProjectNotFoundException); + }); + + it('handles empty issues list gracefully', async () => { + const projectId = faker.number.int(); + const dayToCreate = faker.number.int({ min: 1, max: 5 }); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'find').mockResolvedValue([]); + jest.spyOn(feedbackIssueStatsRepo.manager, 'transaction'); + + await feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ); + + expect(feedbackIssueStatsRepo.manager.transaction).not.toHaveBeenCalled(); + }); + + it('handles transaction errors gracefully', async () => { + const projectId = faker.number.int(); + const dayToCreate = 1; + const issues = [{ id: faker.number.int() }]; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]); + jest.spyOn(feedbackRepo, 'count').mockResolvedValue(1); + jest + .spyOn(feedbackIssueStatsRepo.manager, 'transaction') + .mockRejectedValue(new Error('Transaction failed')); + + // Should not throw error, but log it + await expect( + feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ), + ).resolves.not.toThrow(); + }); + + it('skips creating statistics when feedback count is zero', async () => { + const projectId = faker.number.int(); + const dayToCreate = 1; + const issues = [{ id: faker.number.int() }]; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]); + jest.spyOn(feedbackRepo, 'count').mockResolvedValue(0); + + const transactionSpy = jest.spyOn( + feedbackIssueStatsRepo.manager, + 'transaction', + ); + + await feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ); + + // Transaction is called but no database operations are performed when feedback count is 0 + expect(transactionSpy).toHaveBeenCalledTimes(1); + }); + + it('handles different timezone offsets correctly', async () => { + const projectId = faker.number.int(); + const dayToCreate = 1; + const issues = [{ id: faker.number.int() }]; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'US', + name: 'America/New_York', + offset: '-05:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]); + jest.spyOn(feedbackRepo, 'count').mockResolvedValue(1); + jest + .spyOn(feedbackIssueStatsRepo.manager, 'transaction') + .mockImplementation(async (callback: any) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + await callback(feedbackIssueStatsRepo.manager); + }); + + await feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ); + + expect(feedbackIssueStatsRepo.manager.transaction).toHaveBeenCalledTimes( + 1, + ); + }); + + it('handles large dayToCreate values efficiently', async () => { + const projectId = faker.number.int(); + const dayToCreate = 1000; // Large number + const issues = [{ id: faker.number.int() }]; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]); + jest.spyOn(feedbackRepo, 'count').mockResolvedValue(0); // No feedback to avoid transaction calls + + const transactionSpy = jest.spyOn( + feedbackIssueStatsRepo.manager, + 'transaction', + ); + + await feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ); + + // Transaction is called for each day*issue combination, but no database operations when feedback count is 0 + expect(transactionSpy).toHaveBeenCalledTimes(dayToCreate * issues.length); + }); + + it('handles zero dayToCreate parameter', async () => { + const projectId = faker.number.int(); + const dayToCreate = 0; + const issues = [{ id: faker.number.int() }]; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]); + jest.spyOn(feedbackIssueStatsRepo.manager, 'transaction'); + + await feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ); + + expect(feedbackIssueStatsRepo.manager.transaction).not.toHaveBeenCalled(); + }); + + it('handles transaction timeout scenarios', async () => { + const projectId = faker.number.int(); + const dayToCreate = 1; + const issues = [{ id: faker.number.int() }]; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'find').mockResolvedValue(issues as IssueEntity[]); + jest.spyOn(feedbackRepo, 'count').mockResolvedValue(1); + + // Mock transaction timeout + jest + .spyOn(feedbackIssueStatsRepo.manager, 'transaction') + .mockImplementation(async (_callback) => { + await new Promise((_, reject) => + setTimeout(() => reject(new Error('Transaction timeout')), 100), + ); + }); + + // Should not throw error, but log it + await expect( + feedbackIssueStatsService.createFeedbackIssueStatistics( + projectId, + dayToCreate, + ), + ).resolves.not.toThrow(); + }); }); describe('updateFeedbackCount', () => { @@ -284,15 +846,24 @@ describe('FeedbackIssueStatisticsService suite', () => { const issueId = faker.number.int(); const date = faker.date.past(); const feedbackCount = faker.number.int({ min: 1, max: 10 }); - jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ - id: faker.number.int(), + + const realisticProject = createRealisticProject({ timezone: { offset: '+09:00', - }, - } as ProjectEntity); - jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue({ + countryCode: 'KR', + name: 'Asia/Seoul', + } as any, + }); + + const existingStats = createRealisticFeedbackIssueStats({ + issue: { id: issueId } as any, feedbackCount: 1, - } as FeedbackIssueStatisticsEntity); + }); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(realisticProject); + jest + .spyOn(feedbackIssueStatsRepo, 'findOne') + .mockResolvedValue(existingStats); await feedbackIssueStatsService.updateFeedbackCount({ issueId, @@ -302,9 +873,11 @@ describe('FeedbackIssueStatisticsService suite', () => { expect(feedbackIssueStatsRepo.findOne).toHaveBeenCalledTimes(1); expect(feedbackIssueStatsRepo.save).toHaveBeenCalledTimes(1); - expect(feedbackIssueStatsRepo.save).toHaveBeenCalledWith({ - feedbackCount: 1 + feedbackCount, - }); + expect(feedbackIssueStatsRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + feedbackCount: 1 + feedbackCount, + }), + ); }); it('updating feedback count succeeds with valid inputs and nonexistent date', async () => { const issueId = faker.number.int(); @@ -345,5 +918,211 @@ describe('FeedbackIssueStatisticsService suite', () => { issue: { id: issueId }, }); }); + + it('throws ProjectNotFoundException when project not found', async () => { + const issueId = faker.number.int(); + const date = faker.date.past(); + const feedbackCount = faker.number.int({ min: 1, max: 10 }); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount, + }), + ).rejects.toThrow(ProjectNotFoundException); + }); + + it('returns early when feedbackCount is zero', async () => { + const issueId = faker.number.int(); + const date = faker.date.past(); + const feedbackCount = 0; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); + + await feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount, + }); + + expect(feedbackIssueStatsRepo.findOne).not.toHaveBeenCalled(); + expect(feedbackIssueStatsRepo.save).not.toHaveBeenCalled(); + }); + + it('uses default feedbackCount of 1 when not provided', async () => { + const issueId = faker.number.int(); + const date = faker.date.past(); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue({ + feedbackCount: 1, + } as FeedbackIssueStatisticsEntity); + + await feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount: undefined, + }); + + expect(feedbackIssueStatsRepo.save).toHaveBeenCalledWith({ + feedbackCount: 2, // 1 (existing) + 1 (default) + }); + }); + + it('handles database errors gracefully', async () => { + const issueId = faker.number.int(); + const date = faker.date.past(); + const feedbackCount = faker.number.int({ min: 1, max: 10 }); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); + jest + .spyOn(feedbackIssueStatsRepo, 'findOne') + .mockRejectedValue(new Error('Database error')); + + await expect( + feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount, + }), + ).rejects.toThrow('Database error'); + }); + + it('handles negative feedbackCount values', async () => { + const issueId = faker.number.int(); + const date = faker.date.past(); + const feedbackCount = -5; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue({ + feedbackCount: 10, + } as FeedbackIssueStatisticsEntity); + + await feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount, + }); + + expect(feedbackIssueStatsRepo.save).toHaveBeenCalledWith({ + feedbackCount: 5, // 10 + (-5) + }); + }); + + it('handles very large feedbackCount values', async () => { + const issueId = faker.number.int(); + const date = faker.date.past(); + const feedbackCount = 999999; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue({ + feedbackCount: 1, + } as FeedbackIssueStatisticsEntity); + + await feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount, + }); + + expect(feedbackIssueStatsRepo.save).toHaveBeenCalledWith({ + feedbackCount: 1000000, // 1 + 999999 + }); + }); + + it('handles different timezone offsets in updateFeedbackCount', async () => { + const issueId = faker.number.int(); + const date = faker.date.past(); + const feedbackCount = 1; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '-08:00', // Pacific Time + }, + } as ProjectEntity); + jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue(null); + + const mockQueryBuilder = { + insert: jest.fn().mockReturnThis(), + values: jest.fn().mockReturnThis(), + orUpdate: jest.fn().mockReturnThis(), + updateEntity: jest.fn().mockReturnThis(), + execute: jest.fn().mockResolvedValue({}), + }; + + jest + .spyOn(feedbackIssueStatsRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + await feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount, + }); + + expect(mockQueryBuilder.values).toHaveBeenCalledWith({ + date: new Date( + DateTime.fromJSDate(date).minus({ hours: 8 }).toISO()?.split('T')[0] + + 'T00:00:00', + ), + feedbackCount, + issue: { id: issueId }, + }); + }); + + it('handles edge case date values (leap year, month boundaries)', async () => { + const issueId = faker.number.int(); + const date = new Date('2024-02-29'); // Leap year + const feedbackCount = 1; + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(feedbackIssueStatsRepo, 'findOne').mockResolvedValue({ + feedbackCount: 1, + } as FeedbackIssueStatisticsEntity); + + await feedbackIssueStatsService.updateFeedbackCount({ + issueId, + date, + feedbackCount, + }); + + expect(feedbackIssueStatsRepo.save).toHaveBeenCalledWith({ + feedbackCount: 2, + }); + }); }); }); diff --git a/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.controller.spec.ts b/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.controller.spec.ts index 13fb62aa5..871c42a77 100644 --- a/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.controller.spec.ts +++ b/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.controller.spec.ts @@ -26,7 +26,7 @@ const MockFeedbackStatisticsService = { getIssuedRatio: jest.fn(), }; -describe('Feedback Statistics Controller', () => { +describe('FeedbackStatisticsController', () => { let feedbackStatisticsController: FeedbackStatisticsController; beforeEach(async () => { @@ -45,44 +45,247 @@ describe('Feedback Statistics Controller', () => { ); }); - it('getCountByDateByChannel', async () => { - jest.spyOn(MockFeedbackStatisticsService, 'getCountByDateByChannel'); - const startDate = '2023-01-01'; - const endDate = '2023-12-01'; - const interval = ['day', 'week', 'month'][ - faker.number.int({ min: 0, max: 2 }) - ] as 'day' | 'week' | 'month'; - const channelIds = [faker.number.int(), faker.number.int()]; - - await feedbackStatisticsController.getCountByDateByChannel( - startDate, - endDate, - interval, - channelIds.join(','), - ); + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getCountByDateByChannel', () => { + it('should call service with correct parameters and return transformed response', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const channelIds = '1,2,3'; + const mockServiceResponse = { + channels: [ + { + id: 1, + name: 'Channel 1', + statistics: [ + { + startDate: '2023-01-01', + endDate: '2023-01-01', + count: 10, + }, + ], + }, + ], + }; + + MockFeedbackStatisticsService.getCountByDateByChannel.mockResolvedValue( + mockServiceResponse, + ); + + const result = await feedbackStatisticsController.getCountByDateByChannel( + startDate, + endDate, + interval, + channelIds, + ); + + expect( + MockFeedbackStatisticsService.getCountByDateByChannel, + ).toHaveBeenCalledTimes(1); + expect( + MockFeedbackStatisticsService.getCountByDateByChannel, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + channelIds: [1, 2, 3], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle empty channelIds string', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'week' as const; + const channelIds = ''; + const mockServiceResponse = { channels: [] }; + + MockFeedbackStatisticsService.getCountByDateByChannel.mockResolvedValue( + mockServiceResponse, + ); - expect( - MockFeedbackStatisticsService.getCountByDateByChannel, - ).toHaveBeenCalledTimes(1); + const result = await feedbackStatisticsController.getCountByDateByChannel( + startDate, + endDate, + interval, + channelIds, + ); + + expect( + MockFeedbackStatisticsService.getCountByDateByChannel, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + channelIds: [], + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should filter out invalid channelIds', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'month' as const; + const channelIds = '1,invalid,3,abc'; + const mockServiceResponse = { channels: [] }; + + MockFeedbackStatisticsService.getCountByDateByChannel.mockResolvedValue( + mockServiceResponse, + ); + + const result = await feedbackStatisticsController.getCountByDateByChannel( + startDate, + endDate, + interval, + channelIds, + ); + + expect( + MockFeedbackStatisticsService.getCountByDateByChannel, + ).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + channelIds: [1, 3], + }); + expect(result).toEqual(mockServiceResponse); + }); }); - it('getCount', async () => { - jest.spyOn(MockFeedbackStatisticsService, 'getCountByDateByChannel'); - const from = faker.date.past(); - const to = faker.date.future(); - const projectId = faker.number.int(); - await feedbackStatisticsController.getCount(from, to, projectId); - expect(MockFeedbackStatisticsService.getCount).toHaveBeenCalledTimes(1); + describe('getCount', () => { + it('should call service with correct parameters and return transformed response', async () => { + const from = faker.date.past(); + const to = faker.date.future(); + const projectId = faker.number.int(); + const mockServiceResponse = { count: 42 }; + + MockFeedbackStatisticsService.getCount.mockResolvedValue( + mockServiceResponse, + ); + + const result = await feedbackStatisticsController.getCount( + from, + to, + projectId, + ); + + expect(MockFeedbackStatisticsService.getCount).toHaveBeenCalledTimes(1); + expect(MockFeedbackStatisticsService.getCount).toHaveBeenCalledWith({ + from, + to, + projectId, + }); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle zero count response', async () => { + const from = faker.date.past(); + const to = faker.date.future(); + const projectId = faker.number.int(); + const mockServiceResponse = { count: 0 }; + + MockFeedbackStatisticsService.getCount.mockResolvedValue( + mockServiceResponse, + ); + + const result = await feedbackStatisticsController.getCount( + from, + to, + projectId, + ); + + expect(MockFeedbackStatisticsService.getCount).toHaveBeenCalledWith({ + from, + to, + projectId, + }); + expect(result).toEqual(mockServiceResponse); + }); }); - it('getIssuedRatio', async () => { - jest.spyOn(MockFeedbackStatisticsService, 'getIssuedRatio'); - const from = faker.date.past(); - const to = faker.date.future(); - const projectId = faker.number.int(); - await feedbackStatisticsController.getIssuedRatio(from, to, projectId); - expect(MockFeedbackStatisticsService.getIssuedRatio).toHaveBeenCalledTimes( - 1, - ); + describe('getIssuedRatio', () => { + it('should call service with correct parameters and return transformed response', async () => { + const from = faker.date.past(); + const to = faker.date.future(); + const projectId = faker.number.int(); + const mockServiceResponse = { ratio: 0.75 }; + + MockFeedbackStatisticsService.getIssuedRatio.mockResolvedValue( + mockServiceResponse, + ); + + const result = await feedbackStatisticsController.getIssuedRatio( + from, + to, + projectId, + ); + + expect( + MockFeedbackStatisticsService.getIssuedRatio, + ).toHaveBeenCalledTimes(1); + expect(MockFeedbackStatisticsService.getIssuedRatio).toHaveBeenCalledWith( + { + from, + to, + projectId, + }, + ); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle zero ratio response', async () => { + const from = faker.date.past(); + const to = faker.date.future(); + const projectId = faker.number.int(); + const mockServiceResponse = { ratio: 0 }; + + MockFeedbackStatisticsService.getIssuedRatio.mockResolvedValue( + mockServiceResponse, + ); + + const result = await feedbackStatisticsController.getIssuedRatio( + from, + to, + projectId, + ); + + expect(MockFeedbackStatisticsService.getIssuedRatio).toHaveBeenCalledWith( + { + from, + to, + projectId, + }, + ); + expect(result).toEqual(mockServiceResponse); + }); + + it('should handle maximum ratio response', async () => { + const from = faker.date.past(); + const to = faker.date.future(); + const projectId = faker.number.int(); + const mockServiceResponse = { ratio: 1 }; + + MockFeedbackStatisticsService.getIssuedRatio.mockResolvedValue( + mockServiceResponse, + ); + + const result = await feedbackStatisticsController.getIssuedRatio( + from, + to, + projectId, + ); + + expect(MockFeedbackStatisticsService.getIssuedRatio).toHaveBeenCalledWith( + { + from, + to, + projectId, + }, + ); + expect(result).toEqual(mockServiceResponse); + }); }); }); diff --git a/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.service.spec.ts b/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.service.spec.ts index a6f35fb4c..57a0f3262 100644 --- a/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.service.spec.ts +++ b/apps/api/src/domains/admin/statistics/feedback/feedback-statistics.service.spec.ts @@ -18,15 +18,22 @@ import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { DateTime } from 'luxon'; +import { Between, In } from 'typeorm'; import type { Repository, SelectQueryBuilder } from 'typeorm'; import { ChannelEntity } from '@/domains/admin/channel/channel/channel.entity'; import { FeedbackEntity } from '@/domains/admin/feedback/feedback.entity'; import { IssueEntity } from '@/domains/admin/project/issue/issue.entity'; +import { ProjectNotFoundException } from '@/domains/admin/project/project/exceptions'; import { ProjectEntity } from '@/domains/admin/project/project/project.entity'; +import { SchedulerLockService } from '@/domains/operation/scheduler-lock/scheduler-lock.service'; import { FeedbackStatisticsServiceProviders } from '@/test-utils/providers/feedback-statistics.service.providers'; import { createQueryBuilder, TestConfig } from '@/test-utils/util-functions'; -import { GetCountByDateByChannelDto, GetCountDto } from './dtos'; +import { + GetCountByDateByChannelDto, + GetCountDto, + GetIssuedRateDto, +} from './dtos'; import { FeedbackStatisticsEntity } from './feedback-statistics.entity'; import { FeedbackStatisticsService } from './feedback-statistics.service'; @@ -77,6 +84,7 @@ describe('FeedbackStatisticsService suite', () => { let channelRepo: Repository; let projectRepo: Repository; let schedulerRegistry: SchedulerRegistry; + let schedulerLockService: SchedulerLockService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -95,6 +103,7 @@ describe('FeedbackStatisticsService suite', () => { channelRepo = module.get(getRepositoryToken(ChannelEntity)); projectRepo = module.get(getRepositoryToken(ProjectEntity)); schedulerRegistry = module.get(SchedulerRegistry); + schedulerLockService = module.get(SchedulerLockService); }); describe('getCountByDateByChannel', () => { @@ -112,10 +121,17 @@ describe('FeedbackStatisticsService suite', () => { .spyOn(feedbackStatsRepo, 'find') .mockResolvedValue(feedbackStatsFixture); - const countByDateByChannel = - await feedbackStatsService.getCountByDateByChannel(dto); + const result = await feedbackStatsService.getCountByDateByChannel(dto); - expect(countByDateByChannel).toEqual({ + expect(feedbackStatsRepo.find).toHaveBeenCalledWith({ + where: { + channel: In(channelIds), + date: Between(new Date(startDate), new Date(endDate)), + }, + relations: { channel: true }, + order: { channel: { id: 'ASC' }, date: 'ASC' }, + }); + expect(result).toEqual({ channels: [ { id: 1, @@ -242,11 +258,17 @@ describe('FeedbackStatisticsService suite', () => { .spyOn(feedbackRepo, 'count') .mockResolvedValue(feedbackStatsFixture.length); - const countByDateByChannel = await feedbackStatsService.getCount(dto); + const result = await feedbackStatsService.getCount(dto); - expect(countByDateByChannel).toEqual({ + expect(result).toEqual({ count: feedbackStatsFixture.length, }); + expect(feedbackRepo.count).toHaveBeenCalledWith({ + where: { + createdAt: Between(dto.from, dto.to), + channel: { project: { id: dto.projectId } }, + }, + }); }); }); @@ -255,7 +277,7 @@ describe('FeedbackStatisticsService suite', () => { const from = new Date('2023-01-01'); const to = faker.date.future(); const projectId = faker.number.int(); - const dto = new GetCountDto(); + const dto = new GetIssuedRateDto(); dto.from = from; dto.to = to; dto.projectId = projectId; @@ -272,12 +294,18 @@ describe('FeedbackStatisticsService suite', () => { .spyOn(feedbackRepo, 'count') .mockResolvedValue(feedbackStatsFixture.length); - const countByDateByChannel = - await feedbackStatsService.getIssuedRatio(dto); + const result = await feedbackStatsService.getIssuedRatio(dto); - expect(countByDateByChannel).toEqual({ + expect(result).toEqual({ ratio: 1, }); + expect(issueRepo.createQueryBuilder).toHaveBeenCalledWith('issue'); + expect(feedbackRepo.count).toHaveBeenCalledWith({ + where: { + createdAt: Between(dto.from, dto.to), + channel: { project: { id: dto.projectId } }, + }, + }); }); }); @@ -292,6 +320,11 @@ describe('FeedbackStatisticsService suite', () => { }, } as ProjectEntity); jest.spyOn(schedulerRegistry, 'addCronJob'); + jest.spyOn(schedulerRegistry, 'getCronJobs').mockReturnValue(new Map()); + jest.spyOn(schedulerLockService, 'acquireLock').mockResolvedValue(true); + jest + .spyOn(schedulerLockService, 'releaseLock') + .mockResolvedValue(undefined); await feedbackStatsService.addCronJobByProjectId(projectId); @@ -301,6 +334,15 @@ describe('FeedbackStatisticsService suite', () => { expect.anything(), ); }); + + it('adding a cron job fails when project is not found', async () => { + const projectId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + feedbackStatsService.addCronJobByProjectId(projectId), + ).rejects.toThrow(ProjectNotFoundException); + }); }); describe('createFeedbackStatistics', () => { @@ -323,7 +365,24 @@ describe('FeedbackStatisticsService suite', () => { .mockResolvedValue(channels as ChannelEntity[]); jest.spyOn(feedbackRepo, 'count').mockResolvedValueOnce(0); jest.spyOn(feedbackRepo, 'count').mockResolvedValue(1); - jest.spyOn(feedbackStatsRepo.manager, 'transaction'); + jest + .spyOn(feedbackStatsRepo.manager, 'transaction') + .mockImplementation(async (callback: any) => { + const mockManager = { + createQueryBuilder: jest.fn().mockReturnValue({ + insert: jest.fn().mockReturnThis(), + into: jest.fn().mockReturnThis(), + values: jest.fn().mockReturnThis(), + orUpdate: jest.fn().mockReturnThis(), + updateEntity: jest.fn().mockReturnThis(), + execute: jest.fn().mockResolvedValue({}), + }), + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await (callback as (manager: any) => Promise)( + mockManager, + ); + }); await feedbackStatsService.createFeedbackStatistics( projectId, @@ -334,6 +393,16 @@ describe('FeedbackStatisticsService suite', () => { dayToCreate * channelCount, ); }); + + it('creating feedback statistics fails when project is not found', async () => { + const projectId = faker.number.int(); + const dayToCreate = faker.number.int({ min: 1, max: 5 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + feedbackStatsService.createFeedbackStatistics(projectId, dayToCreate), + ).rejects.toThrow(ProjectNotFoundException); + }); }); describe('updateCount', () => { @@ -350,6 +419,9 @@ describe('FeedbackStatisticsService suite', () => { jest.spyOn(feedbackStatsRepo, 'findOne').mockResolvedValue({ count: 1, } as FeedbackStatisticsEntity); + jest.spyOn(feedbackStatsRepo, 'save').mockResolvedValue({ + count: 1 + count, + } as FeedbackStatisticsEntity); await feedbackStatsService.updateCount({ channelId, @@ -380,7 +452,12 @@ describe('FeedbackStatisticsService suite', () => { () => createQueryBuilder as unknown as SelectQueryBuilder, ); - jest.spyOn(createQueryBuilder, 'values' as never); + jest.spyOn(createQueryBuilder, 'values' as never).mockReturnThis(); + jest.spyOn(createQueryBuilder, 'orUpdate' as never).mockReturnThis(); + jest.spyOn(createQueryBuilder, 'updateEntity' as never).mockReturnThis(); + jest + .spyOn(createQueryBuilder, 'execute' as never) + .mockResolvedValue({} as never); await feedbackStatsService.updateCount({ channelId, @@ -400,5 +477,38 @@ describe('FeedbackStatisticsService suite', () => { channel: { id: channelId }, }); }); + + it('updating count fails when project is not found', async () => { + const channelId = faker.number.int(); + const date = faker.date.past(); + const count = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + feedbackStatsService.updateCount({ + channelId, + date, + count, + }), + ).rejects.toThrow(ProjectNotFoundException); + }); + + it('updating count with zero count does nothing', async () => { + const channelId = faker.number.int(); + const date = faker.date.past(); + const count = 0; + + const projectRepoSpy = jest.spyOn(projectRepo, 'findOne'); + const feedbackStatsRepoSpy = jest.spyOn(feedbackStatsRepo, 'findOne'); + + await feedbackStatsService.updateCount({ + channelId, + date, + count, + }); + + expect(projectRepoSpy).not.toHaveBeenCalled(); + expect(feedbackStatsRepoSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/apps/api/src/domains/admin/statistics/issue/issue-statistics.controller.spec.ts b/apps/api/src/domains/admin/statistics/issue/issue-statistics.controller.spec.ts index 7c35533ec..3aeb0d3bc 100644 --- a/apps/api/src/domains/admin/statistics/issue/issue-statistics.controller.spec.ts +++ b/apps/api/src/domains/admin/statistics/issue/issue-statistics.controller.spec.ts @@ -23,13 +23,16 @@ import { IssueStatisticsService } from './issue-statistics.service'; const MockIssueStatisticsService = { getCountByDate: jest.fn(), getCount: jest.fn(), - getIssuedRatio: jest.fn(), + getCountByStatus: jest.fn(), }; describe('Issue Statistics Controller', () => { let issueStatisticsController: IssueStatisticsController; beforeEach(async () => { + // Mock 초기화 + jest.clearAllMocks(); + const module = await Test.createTestingModule({ controllers: [IssueStatisticsController], providers: [ @@ -50,8 +53,24 @@ describe('Issue Statistics Controller', () => { faker.number.int({ min: 0, max: 2 }) ] as 'day' | 'week' | 'month'; const projectId = faker.number.int(); + const mockResult = { + statistics: [ + { + startDate: '2023-01-01', + endDate: '2023-01-01', + count: faker.number.int({ min: 0, max: 100 }), + }, + { + startDate: '2023-01-02', + endDate: '2023-01-02', + count: faker.number.int({ min: 0, max: 100 }), + }, + ], + }; - await issueStatisticsController.getCountByDate( + MockIssueStatisticsService.getCountByDate.mockResolvedValue(mockResult); + + const result = await issueStatisticsController.getCountByDate( startDate, endDate, interval, @@ -59,14 +78,205 @@ describe('Issue Statistics Controller', () => { ); expect(MockIssueStatisticsService.getCountByDate).toHaveBeenCalledTimes(1); + expect(MockIssueStatisticsService.getCountByDate).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + projectId, + }); + expect(result).toEqual(mockResult); }); it('getCount', async () => { - jest.spyOn(MockIssueStatisticsService, 'getCountByDate'); + jest.spyOn(MockIssueStatisticsService, 'getCount'); const from = faker.date.past(); const to = faker.date.future(); const projectId = faker.number.int(); - await issueStatisticsController.getCount(from, to, projectId); + const mockResult = { count: faker.number.int({ min: 0, max: 1000 }) }; + + MockIssueStatisticsService.getCount.mockResolvedValue(mockResult); + + const result = await issueStatisticsController.getCount( + from, + to, + projectId, + ); + expect(MockIssueStatisticsService.getCount).toHaveBeenCalledTimes(1); + expect(MockIssueStatisticsService.getCount).toHaveBeenCalledWith({ + from, + to, + projectId, + }); + expect(result).toEqual(mockResult); + }); + + it('getCountByStatus', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCountByStatus'); + const projectId = faker.number.int(); + const mockResult = { + statistics: [ + { status: 'OPEN', count: faker.number.int({ min: 0, max: 100 }) }, + { status: 'CLOSED', count: faker.number.int({ min: 0, max: 100 }) }, + { + status: 'IN_PROGRESS', + count: faker.number.int({ min: 0, max: 100 }), + }, + ], + }; + + MockIssueStatisticsService.getCountByStatus.mockResolvedValue(mockResult); + + const result = await issueStatisticsController.getCountByStatus(projectId); + + expect(MockIssueStatisticsService.getCountByStatus).toHaveBeenCalledTimes( + 1, + ); + expect(MockIssueStatisticsService.getCountByStatus).toHaveBeenCalledWith({ + projectId, + }); + expect(result).toEqual(mockResult); + }); + + describe('Edge Cases', () => { + it('getCountByDate with empty statistics', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCountByDate'); + const startDate = '2023-01-01'; + const endDate = '2023-01-02'; + const interval = 'day' as const; + const projectId = faker.number.int(); + const mockResult = { statistics: [] }; + + MockIssueStatisticsService.getCountByDate.mockResolvedValue(mockResult); + + const result = await issueStatisticsController.getCountByDate( + startDate, + endDate, + interval, + projectId, + ); + + expect(result).toEqual(mockResult); + }); + + it('getCountByStatus with empty statistics', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCountByStatus'); + const projectId = faker.number.int(); + const mockResult = { statistics: [] }; + + MockIssueStatisticsService.getCountByStatus.mockResolvedValue(mockResult); + + const result = + await issueStatisticsController.getCountByStatus(projectId); + + expect(result).toEqual(mockResult); + }); + + it('getCount with zero count', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCount'); + const from = faker.date.past(); + const to = faker.date.future(); + const projectId = faker.number.int(); + const mockResult = { count: 0 }; + + MockIssueStatisticsService.getCount.mockResolvedValue(mockResult); + + const result = await issueStatisticsController.getCount( + from, + to, + projectId, + ); + + expect(result).toEqual(mockResult); + }); + + it('getCountByDate with different interval types', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCountByDate'); + const startDate = '2023-01-01'; + const endDate = '2023-12-31'; + const projectId = faker.number.int(); + + const intervals: ('day' | 'week' | 'month')[] = ['day', 'week', 'month']; + + for (const interval of intervals) { + const mockResult = { + statistics: [ + { + startDate: '2023-01-01', + endDate: + interval === 'day' ? '2023-01-01' + : interval === 'week' ? '2023-01-07' + : '2023-01-31', + count: faker.number.int({ min: 0, max: 100 }), + }, + ], + }; + + MockIssueStatisticsService.getCountByDate.mockResolvedValue(mockResult); + + const result = await issueStatisticsController.getCountByDate( + startDate, + endDate, + interval, + projectId, + ); + + expect(MockIssueStatisticsService.getCountByDate).toHaveBeenCalledWith({ + startDate, + endDate, + interval, + projectId, + }); + expect(result).toEqual(mockResult); + } + }); + }); + + describe('Error Handling', () => { + it('getCount should handle service errors', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCount'); + const from = faker.date.past(); + const to = faker.date.future(); + const projectId = faker.number.int(); + const error = new Error('Database connection failed'); + + MockIssueStatisticsService.getCount.mockRejectedValue(error); + + await expect( + issueStatisticsController.getCount(from, to, projectId), + ).rejects.toThrow('Database connection failed'); + }); + + it('getCountByDate should handle service errors', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCountByDate'); + const startDate = '2023-01-01'; + const endDate = '2023-12-01'; + const interval = 'day' as const; + const projectId = faker.number.int(); + const error = new Error('Invalid date range'); + + MockIssueStatisticsService.getCountByDate.mockRejectedValue(error); + + await expect( + issueStatisticsController.getCountByDate( + startDate, + endDate, + interval, + projectId, + ), + ).rejects.toThrow('Invalid date range'); + }); + + it('getCountByStatus should handle service errors', async () => { + jest.spyOn(MockIssueStatisticsService, 'getCountByStatus'); + const projectId = faker.number.int(); + const error = new Error('Project not found'); + + MockIssueStatisticsService.getCountByStatus.mockRejectedValue(error); + + await expect( + issueStatisticsController.getCountByStatus(projectId), + ).rejects.toThrow('Project not found'); + }); }); }); diff --git a/apps/api/src/domains/admin/statistics/issue/issue-statistics.service.spec.ts b/apps/api/src/domains/admin/statistics/issue/issue-statistics.service.spec.ts index b3aeea03b..e51ac9c12 100644 --- a/apps/api/src/domains/admin/statistics/issue/issue-statistics.service.spec.ts +++ b/apps/api/src/domains/admin/statistics/issue/issue-statistics.service.spec.ts @@ -19,12 +19,13 @@ import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { DateTime } from 'luxon'; import type { Repository, SelectQueryBuilder } from 'typeorm'; +import { Between } from 'typeorm'; import { IssueEntity } from '@/domains/admin/project/issue/issue.entity'; import { ProjectEntity } from '@/domains/admin/project/project/project.entity'; import { IssueStatisticsServiceProviders } from '@/test-utils/providers/issue-statistics.service.providers'; import { createQueryBuilder, TestConfig } from '@/test-utils/util-functions'; -import { GetCountByDateDto, GetCountDto } from './dtos'; +import { GetCountByDateDto, GetCountByStatusDto, GetCountDto } from './dtos'; import { IssueStatisticsEntity } from './issue-statistics.entity'; import { IssueStatisticsService } from './issue-statistics.service'; @@ -105,6 +106,13 @@ describe('IssueStatisticsService suite', () => { const countByDateByChannel = await issueStatsService.getCountByDate(dto); expect(issueStatsRepo.find).toHaveBeenCalledTimes(1); + expect(issueStatsRepo.find).toHaveBeenCalledWith({ + where: { + date: Between(new Date(startDate), new Date(endDate)), + project: { id: projectId }, + }, + order: { date: 'ASC' }, + }); expect(countByDateByChannel).toEqual({ statistics: [ { @@ -195,6 +203,51 @@ describe('IssueStatisticsService suite', () => { ], }); }); + + it('getting counts by date returns empty statistics when no data found', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-12-31'; + const interval = 'day'; + const projectId = faker.number.int(); + const dto = new GetCountByDateDto(); + dto.startDate = startDate; + dto.endDate = endDate; + dto.interval = interval; + dto.projectId = projectId; + jest.spyOn(issueStatsRepo, 'find').mockResolvedValue([]); + + const result = await issueStatsService.getCountByDate(dto); + + expect(issueStatsRepo.find).toHaveBeenCalledTimes(1); + expect(result).toEqual({ statistics: [] }); + }); + + it('getting counts by date handles single day interval correctly', async () => { + const startDate = '2023-01-01'; + const endDate = '2023-01-01'; + const interval = 'day'; + const projectId = faker.number.int(); + const dto = new GetCountByDateDto(); + dto.startDate = startDate; + dto.endDate = endDate; + dto.interval = interval; + dto.projectId = projectId; + const singleDayFixture = [issueStatsFixture[0]]; + jest.spyOn(issueStatsRepo, 'find').mockResolvedValue(singleDayFixture); + + const result = await issueStatsService.getCountByDate(dto); + + expect(issueStatsRepo.find).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + statistics: [ + { + count: 1, + startDate: '2023-01-01', + endDate: '2023-01-01', + }, + ], + }); + }); }); describe('getCount', () => { @@ -213,10 +266,104 @@ describe('IssueStatisticsService suite', () => { const countByDateByChannel = await issueStatsService.getCount(dto); expect(issueRepo.count).toHaveBeenCalledTimes(1); + expect(issueRepo.count).toHaveBeenCalledWith({ + where: { + createdAt: Between(from, to), + project: { id: projectId }, + }, + }); expect(countByDateByChannel).toEqual({ count: issueStatsFixture.length, }); }); + + it('getting count returns zero when no issues found', async () => { + const from = new Date('2023-01-01'); + const to = new Date('2023-12-31'); + const projectId = faker.number.int(); + const dto = new GetCountDto(); + dto.from = from; + dto.to = to; + dto.projectId = projectId; + jest.spyOn(issueRepo, 'count').mockResolvedValue(0); + + const result = await issueStatsService.getCount(dto); + + expect(issueRepo.count).toHaveBeenCalledTimes(1); + expect(result).toEqual({ count: 0 }); + }); + }); + + describe('getCountByStatus', () => { + it('getting count by status succeeds with valid inputs', async () => { + const projectId = faker.number.int(); + const dto = new GetCountByStatusDto(); + dto.projectId = projectId; + + const mockRawResults = [ + { status: 'OPEN', count: '5' }, + { status: 'CLOSED', count: '3' }, + { status: 'IN_PROGRESS', count: '2' }, + ]; + + const mockQueryBuilder = { + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + getRawMany: jest.fn().mockResolvedValue(mockRawResults), + }; + + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await issueStatsService.getCountByStatus(dto); + + expect(issueRepo.createQueryBuilder).toHaveBeenCalledWith('issue'); + expect(mockQueryBuilder.select).toHaveBeenCalledWith( + 'issue.status', + 'status', + ); + expect(mockQueryBuilder.addSelect).toHaveBeenCalledWith( + 'COUNT(issue.id)', + 'count', + ); + expect(mockQueryBuilder.where).toHaveBeenCalledWith( + 'issue.project_id = :projectId', + { projectId }, + ); + expect(mockQueryBuilder.groupBy).toHaveBeenCalledWith('issue.status'); + expect(result).toEqual({ + statistics: [ + { status: 'OPEN', count: 5 }, + { status: 'CLOSED', count: 3 }, + { status: 'IN_PROGRESS', count: 2 }, + ], + }); + }); + + it('getting count by status returns empty array when no issues found', async () => { + const projectId = faker.number.int(); + const dto = new GetCountByStatusDto(); + dto.projectId = projectId; + + const mockQueryBuilder = { + select: jest.fn().mockReturnThis(), + addSelect: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + getRawMany: jest.fn().mockResolvedValue([]), + }; + + jest + .spyOn(issueRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await issueStatsService.getCountByStatus(dto); + + expect(result).toEqual({ statistics: [] }); + }); }); describe('addCronJobByProjectId', () => { @@ -230,6 +377,7 @@ describe('IssueStatisticsService suite', () => { }, } as ProjectEntity); jest.spyOn(schedulerRegistry, 'addCronJob'); + jest.spyOn(schedulerRegistry, 'getCronJobs').mockReturnValue(new Map()); await issueStatsService.addCronJobByProjectId(projectId); @@ -239,6 +387,37 @@ describe('IssueStatisticsService suite', () => { expect.anything(), ); }); + + it('adding a cron job throws NotFoundException when project not found', async () => { + const projectId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + issueStatsService.addCronJobByProjectId(projectId), + ).rejects.toThrow(`Project(id: ${projectId}) not found`); + }); + + it('adding a cron job skips when cron job already exists', async () => { + const projectId = faker.number.int(); + const existingCronJobs = new Map(); + existingCronJobs.set(`issue-statistics-${projectId}`, {}); + + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest + .spyOn(schedulerRegistry, 'getCronJobs') + .mockReturnValue(existingCronJobs); + jest.spyOn(schedulerRegistry, 'addCronJob'); + + await issueStatsService.addCronJobByProjectId(projectId); + + expect(schedulerRegistry.addCronJob).not.toHaveBeenCalled(); + }); }); describe('createIssueStatistics', () => { @@ -262,6 +441,89 @@ describe('IssueStatisticsService suite', () => { dayToCreate, ); }); + + it('creating issue statistics throws NotFoundException when project not found', async () => { + const projectId = faker.number.int(); + const dayToCreate = faker.number.int({ min: 1, max: 5 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + issueStatsService.createIssueStatistics(projectId, dayToCreate), + ).rejects.toThrow(`Project(id: ${projectId}) not found`); + }); + + it('creating issue statistics skips transaction when issue count is zero', async () => { + const projectId = faker.number.int(); + const dayToCreate = faker.number.int({ min: 1, max: 3 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'count').mockResolvedValue(0); + jest.spyOn(issueStatsRepo.manager, 'transaction'); + + await issueStatsService.createIssueStatistics(projectId, dayToCreate); + + expect(issueStatsRepo.manager.transaction).toHaveBeenCalledTimes( + dayToCreate, + ); + }); + + it('creating issue statistics handles default dayToCreate parameter', async () => { + const projectId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'count').mockResolvedValue(1); + jest.spyOn(issueStatsRepo.manager, 'transaction'); + + await issueStatsService.createIssueStatistics(projectId); + + expect(issueStatsRepo.manager.transaction).toHaveBeenCalledTimes(1); + }); + + it('creating issue statistics handles negative timezone offset', async () => { + const projectId = faker.number.int(); + const dayToCreate = faker.number.int({ min: 1, max: 3 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'US', + name: 'America/New_York', + offset: '-05:00', + }, + } as ProjectEntity); + jest.spyOn(issueRepo, 'count').mockResolvedValue(1); + jest.spyOn(issueStatsRepo.manager, 'transaction'); + + await issueStatsService.createIssueStatistics(projectId, dayToCreate); + + expect(issueStatsRepo.manager.transaction).toHaveBeenCalledTimes( + dayToCreate, + ); + }); + + it('creating issue statistics handles zero dayToCreate', async () => { + const projectId = faker.number.int(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + timezone: { + countryCode: 'KR', + name: 'Asia/Seoul', + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueStatsRepo.manager, 'transaction'); + + await issueStatsService.createIssueStatistics(projectId, 0); + + expect(issueStatsRepo.manager.transaction).not.toHaveBeenCalled(); + }); }); describe('updateCount', () => { @@ -286,11 +548,18 @@ describe('IssueStatisticsService suite', () => { }); expect(issueStatsRepo.findOne).toHaveBeenCalledTimes(1); + expect(issueStatsRepo.findOne).toHaveBeenCalledWith({ + where: { + date: expect.any(Date), + project: { id: projectId }, + }, + }); expect(issueStatsRepo.save).toHaveBeenCalledTimes(1); expect(issueStatsRepo.save).toHaveBeenCalledWith({ count: 1 + count, }); }); + it('updating count succeeds with valid inputs and nonexistent date', async () => { const projectId = faker.number.int(); const date = faker.date.past(); @@ -317,6 +586,12 @@ describe('IssueStatisticsService suite', () => { }); expect(issueStatsRepo.findOne).toHaveBeenCalledTimes(1); + expect(issueStatsRepo.findOne).toHaveBeenCalledWith({ + where: { + date: expect.any(Date), + project: { id: projectId }, + }, + }); expect(issueStatsRepo.createQueryBuilder).toHaveBeenCalledTimes(1); expect(createQueryBuilder.values).toHaveBeenCalledTimes(1); expect(createQueryBuilder.values).toHaveBeenCalledWith({ @@ -328,5 +603,75 @@ describe('IssueStatisticsService suite', () => { project: { id: projectId }, }); }); + + it('updating count throws NotFoundException when project not found', async () => { + const projectId = faker.number.int(); + const date = faker.date.past(); + const count = faker.number.int({ min: 1, max: 10 }); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue(null); + + await expect( + issueStatsService.updateCount({ + projectId, + date, + count, + }), + ).rejects.toThrow(`Project(id: ${projectId}) not found`); + }); + + it('updating count returns early when count is zero', async () => { + const projectId = faker.number.int(); + const date = faker.date.past(); + const count = 0; + jest.spyOn(projectRepo, 'findOne'); + + await issueStatsService.updateCount({ + projectId, + date, + count, + }); + + expect(projectRepo.findOne).not.toHaveBeenCalled(); + }); + + it('updating count uses default count of 1 when count is undefined', async () => { + const projectId = faker.number.int(); + const date = faker.date.past(); + jest.spyOn(projectRepo, 'findOne').mockResolvedValue({ + id: faker.number.int(), + timezone: { + offset: '+09:00', + }, + } as ProjectEntity); + jest.spyOn(issueStatsRepo, 'findOne').mockResolvedValue(null); + + const mockQueryBuilder = { + insert: jest.fn().mockReturnThis(), + into: jest.fn().mockReturnThis(), + values: jest.fn().mockReturnThis(), + orUpdate: jest.fn().mockReturnThis(), + updateEntity: jest.fn().mockReturnThis(), + execute: jest.fn().mockResolvedValue({}), + }; + + jest + .spyOn(issueStatsRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + await issueStatsService.updateCount({ + projectId, + date, + count: undefined, + }); + + expect(mockQueryBuilder.values).toHaveBeenCalledWith({ + date: new Date( + DateTime.fromJSDate(date).plus({ hours: 9 }).toISO()?.split('T')[0] + + 'T00:00:00', + ), + count: 1, + project: { id: projectId }, + }); + }); }); }); diff --git a/apps/api/src/domains/admin/statistics/utils/util-functions.spec.ts b/apps/api/src/domains/admin/statistics/utils/util-functions.spec.ts new file mode 100644 index 000000000..eb741fa78 --- /dev/null +++ b/apps/api/src/domains/admin/statistics/utils/util-functions.spec.ts @@ -0,0 +1,236 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { getIntervalDatesInFormat } from './util-functions'; + +describe('getIntervalDatesInFormat', () => { + describe('Error cases', () => { + it('should throw error when startDate is later than endDate', () => { + const startDate = '2024-01-15'; + const endDate = '2024-01-10'; + const inputDate = new Date('2024-01-12'); + + expect(() => { + getIntervalDatesInFormat(startDate, endDate, inputDate, 'day'); + }).toThrow('endDate must be later than startDate'); + }); + }); + + describe('Day interval tests', () => { + it('should return same start and end date for day interval', () => { + const startDate = '2024-01-01'; + const endDate = '2024-01-31'; + const inputDate = new Date('2024-01-15'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'day', + ); + + expect(result).toEqual({ + startOfInterval: '2024-01-15', + endOfInterval: '2024-01-15', + }); + }); + + it('should return correct result for different dates in day interval', () => { + const startDate = '2024-01-01'; + const endDate = '2024-01-31'; + const inputDate = new Date('2024-01-25'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'day', + ); + + expect(result).toEqual({ + startOfInterval: '2024-01-25', + endOfInterval: '2024-01-25', + }); + }); + }); + + describe('Week interval tests', () => { + it('should return correct week range for week interval', () => { + const startDate = '2024-01-01'; + const endDate = '2024-01-31'; + const inputDate = new Date('2024-01-15'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'week', + ); + + expect(result.startOfInterval).toBeDefined(); + expect(result.endOfInterval).toBeDefined(); + expect(result.startOfInterval).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(result.endOfInterval).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); + + it('should use startDate when input date is before startDate in week interval', () => { + const startDate = '2024-01-10'; + const endDate = '2024-01-31'; + const inputDate = new Date('2024-01-05'); // Before startDate + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'week', + ); + + expect(result.startOfInterval).toBe('2024-01-10'); + }); + }); + + describe('Month interval tests', () => { + it('should return correct month range for month interval', () => { + const startDate = '2024-01-01'; + const endDate = '2024-03-31'; + const inputDate = new Date('2024-02-15'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'month', + ); + + expect(result.startOfInterval).toBeDefined(); + expect(result.endOfInterval).toBeDefined(); + expect(result.startOfInterval).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(result.endOfInterval).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); + + it('should use startDate when input date is before startDate in month interval', () => { + const startDate = '2024-02-01'; + const endDate = '2024-03-31'; + const inputDate = new Date('2024-01-15'); // Before startDate + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'month', + ); + + expect(result.startOfInterval).toBe('2024-02-01'); + }); + }); + + describe('Edge case tests', () => { + it('should handle same start and end date correctly', () => { + const startDate = '2024-01-15'; + const endDate = '2024-01-15'; + const inputDate = new Date('2024-01-15'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'day', + ); + + expect(result).toEqual({ + startOfInterval: '2024-01-15', + endOfInterval: '2024-01-15', + }); + }); + + it('should handle intervalCount of 0 correctly for week interval', () => { + const startDate = '2024-01-15'; + const endDate = '2024-01-15'; + const inputDate = new Date('2024-01-15'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'week', + ); + + expect(result.startOfInterval).toBeDefined(); + expect(result.endOfInterval).toBeDefined(); + }); + + it('should handle intervalCount of 0 correctly for month interval', () => { + const startDate = '2024-01-15'; + const endDate = '2024-01-15'; + const inputDate = new Date('2024-01-15'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'month', + ); + + expect(result.startOfInterval).toBeDefined(); + expect(result.endOfInterval).toBeDefined(); + }); + }); + + describe('Actual date calculation validation', () => { + it('should calculate accurate week range for week interval', () => { + const startDate = '2024-01-01'; + const endDate = '2024-01-31'; + const inputDate = new Date('2024-01-15'); // Monday + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'week', + ); + + // Verify that results are valid date formats + expect(new Date(result.startOfInterval)).toBeInstanceOf(Date); + expect(new Date(result.endOfInterval)).toBeInstanceOf(Date); + + // Start date should be earlier than or equal to end date + expect(new Date(result.startOfInterval).getTime()).toBeLessThanOrEqual( + new Date(result.endOfInterval).getTime(), + ); + }); + + it('should calculate accurate month range for month interval', () => { + const startDate = '2024-01-01'; + const endDate = '2024-03-31'; + const inputDate = new Date('2024-02-15'); + + const result = getIntervalDatesInFormat( + startDate, + endDate, + inputDate, + 'month', + ); + + // Verify that results are valid date formats + expect(new Date(result.startOfInterval)).toBeInstanceOf(Date); + expect(new Date(result.endOfInterval)).toBeInstanceOf(Date); + + // Start date should be earlier than or equal to end date + expect(new Date(result.startOfInterval).getTime()).toBeLessThanOrEqual( + new Date(result.endOfInterval).getTime(), + ); + }); + }); +}); diff --git a/apps/api/src/domains/admin/tenant/tenant.controller.spec.ts b/apps/api/src/domains/admin/tenant/tenant.controller.spec.ts new file mode 100644 index 000000000..1b1102a08 --- /dev/null +++ b/apps/api/src/domains/admin/tenant/tenant.controller.spec.ts @@ -0,0 +1,576 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { faker } from '@faker-js/faker'; +import { Test } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; + +import { getMockProvider, MockDataSource } from '@/test-utils/util-functions'; +import { SetupTenantRequestDto, UpdateTenantRequestDto } from './dtos/requests'; +import { + CountFeedbacksByTenantIdResponseDto, + GetTenantResponseDto, +} from './dtos/responses'; +import { LoginButtonTypeEnum } from './entities/enums'; +import { TenantController } from './tenant.controller'; +import { TenantService } from './tenant.service'; + +const MockTenantService = { + create: jest.fn(), + update: jest.fn(), + findOne: jest.fn(), + countByTenantId: jest.fn(), +}; + +describe('TenantController', () => { + let tenantController: TenantController; + + beforeEach(async () => { + // Reset all mocks before each test + jest.clearAllMocks(); + + const module = await Test.createTestingModule({ + controllers: [TenantController], + providers: [ + getMockProvider(TenantService, MockTenantService), + getMockProvider(DataSource, MockDataSource), + ], + }).compile(); + + tenantController = module.get(TenantController); + }); + + describe('setup', () => { + it('should create a new tenant successfully', async () => { + const mockTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + createdAt: faker.date.past(), + }; + + MockTenantService.create.mockResolvedValue(mockTenant); + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.email = faker.internet.email(); + dto.password = '12345678'; + + await tenantController.setup(dto); + + expect(MockTenantService.create).toHaveBeenCalledTimes(1); + expect(MockTenantService.create).toHaveBeenCalledWith(dto); + }); + + it('should handle tenant setup with valid email format', async () => { + const mockTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + createdAt: faker.date.past(), + }; + + MockTenantService.create.mockResolvedValue(mockTenant); + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.email = 'test@example.com'; + dto.password = 'ValidPass123!'; + + await tenantController.setup(dto); + + expect(MockTenantService.create).toHaveBeenCalledTimes(1); + expect(MockTenantService.create).toHaveBeenCalledWith(dto); + }); + }); + + describe('update', () => { + it('should update tenant successfully', async () => { + const mockUpdatedTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + useEmail: true, + useOAuth: false, + allowDomains: ['example.com'], + oauthConfig: null, + updatedAt: faker.date.recent(), + }; + + MockTenantService.update.mockResolvedValue(mockUpdatedTenant); + + const dto = new UpdateTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.useEmail = true; + dto.useOAuth = false; + dto.allowDomains = ['example.com']; + dto.oauthConfig = null; + + await tenantController.update(dto); + + expect(MockTenantService.update).toHaveBeenCalledTimes(1); + expect(MockTenantService.update).toHaveBeenCalledWith(dto); + }); + + it('should handle tenant update with OAuth configuration', async () => { + const mockUpdatedTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + useEmail: false, + useOAuth: true, + allowDomains: null, + oauthConfig: { + clientId: faker.string.alphanumeric(20), + clientSecret: faker.string.alphanumeric(30), + authCodeRequestURL: faker.internet.url(), + scopeString: 'read write', + accessTokenRequestURL: faker.internet.url(), + userProfileRequestURL: faker.internet.url(), + emailKey: 'email', + loginButtonType: 'GOOGLE', + loginButtonName: 'Google Login', + }, + updatedAt: faker.date.recent(), + }; + + MockTenantService.update.mockResolvedValue(mockUpdatedTenant); + + const dto = new UpdateTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.useEmail = false; + dto.useOAuth = true; + dto.allowDomains = null; + dto.oauthConfig = { + clientId: faker.string.alphanumeric(20), + clientSecret: faker.string.alphanumeric(30), + authCodeRequestURL: faker.internet.url(), + scopeString: 'read write', + accessTokenRequestURL: faker.internet.url(), + userProfileRequestURL: faker.internet.url(), + emailKey: 'email', + loginButtonType: LoginButtonTypeEnum.GOOGLE, + loginButtonName: 'Google Login', + }; + + await tenantController.update(dto); + + expect(MockTenantService.update).toHaveBeenCalledTimes(1); + expect(MockTenantService.update).toHaveBeenCalledWith(dto); + }); + + it('should handle tenant update with minimal data', async () => { + const mockUpdatedTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + description: null, + useEmail: false, + useOAuth: false, + allowDomains: null, + oauthConfig: null, + updatedAt: faker.date.recent(), + }; + + MockTenantService.update.mockResolvedValue(mockUpdatedTenant); + + const dto = new UpdateTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.description = null; + dto.useEmail = false; + dto.useOAuth = false; + dto.allowDomains = null; + dto.oauthConfig = null; + + await tenantController.update(dto); + + expect(MockTenantService.update).toHaveBeenCalledTimes(1); + expect(MockTenantService.update).toHaveBeenCalledWith(dto); + }); + }); + + describe('get', () => { + it('should return tenant information successfully', async () => { + const mockTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + useEmail: true, + useOAuth: false, + allowDomains: ['example.com'], + oauthConfig: null, + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + }; + + MockTenantService.findOne.mockResolvedValue(mockTenant); + + const result = await tenantController.get(); + + expect(MockTenantService.findOne).toHaveBeenCalledTimes(1); + expect(MockTenantService.findOne).toHaveBeenCalledWith(); + expect(result).toBeInstanceOf(GetTenantResponseDto); + expect(result.id).toBe(mockTenant.id); + expect(result.siteName).toBe(mockTenant.siteName); + expect(result.description).toBe(mockTenant.description); + expect(result.useEmail).toBe(mockTenant.useEmail); + expect(result.useOAuth).toBe(mockTenant.useOAuth); + expect(result.allowDomains).toEqual(mockTenant.allowDomains); + expect(result.oauthConfig).toBe(mockTenant.oauthConfig); + }); + + it('should return tenant with OAuth configuration', async () => { + const mockTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + useEmail: false, + useOAuth: true, + allowDomains: null, + oauthConfig: { + oauthUse: true, + clientId: faker.string.alphanumeric(20), + clientSecret: faker.string.alphanumeric(30), + authCodeRequestURL: faker.internet.url(), + scopeString: 'read write', + accessTokenRequestURL: faker.internet.url(), + userProfileRequestURL: faker.internet.url(), + emailKey: 'email', + loginButtonType: 'GOOGLE', + loginButtonName: 'Google Login', + }, + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + }; + + MockTenantService.findOne.mockResolvedValue(mockTenant); + + const result = await tenantController.get(); + + expect(MockTenantService.findOne).toHaveBeenCalledTimes(1); + expect(MockTenantService.findOne).toHaveBeenCalledWith(); + expect(result).toBeInstanceOf(GetTenantResponseDto); + expect(result.id).toBe(mockTenant.id); + expect(result.siteName).toBe(mockTenant.siteName); + expect(result.useOAuth).toBe(mockTenant.useOAuth); + expect(result.oauthConfig).toBeDefined(); + expect(result.oauthConfig?.clientId).toBe( + mockTenant.oauthConfig.clientId, + ); + }); + }); + + describe('countFeedbacks', () => { + it('should return feedback count by tenant id successfully', async () => { + const tenantId = faker.number.int(); + const mockCount = { + total: faker.number.int({ min: 0, max: 1000 }), + }; + + MockTenantService.countByTenantId.mockResolvedValue(mockCount); + + const result = await tenantController.countFeedbacks(tenantId); + + expect(MockTenantService.countByTenantId).toHaveBeenCalledTimes(1); + expect(MockTenantService.countByTenantId).toHaveBeenCalledWith({ + tenantId, + }); + expect(result).toBeInstanceOf(CountFeedbacksByTenantIdResponseDto); + expect(result.total).toBe(mockCount.total); + }); + + it('should return zero count when no feedbacks exist', async () => { + const tenantId = faker.number.int(); + const mockCount = { + total: 0, + }; + + MockTenantService.countByTenantId.mockResolvedValue(mockCount); + + const result = await tenantController.countFeedbacks(tenantId); + + expect(MockTenantService.countByTenantId).toHaveBeenCalledTimes(1); + expect(MockTenantService.countByTenantId).toHaveBeenCalledWith({ + tenantId, + }); + expect(result).toBeInstanceOf(CountFeedbacksByTenantIdResponseDto); + expect(result.total).toBe(0); + }); + + it('should handle large feedback counts', async () => { + const tenantId = faker.number.int(); + const mockCount = { + total: faker.number.int({ min: 10000, max: 100000 }), + }; + + MockTenantService.countByTenantId.mockResolvedValue(mockCount); + + const result = await tenantController.countFeedbacks(tenantId); + + expect(MockTenantService.countByTenantId).toHaveBeenCalledTimes(1); + expect(MockTenantService.countByTenantId).toHaveBeenCalledWith({ + tenantId, + }); + expect(result).toBeInstanceOf(CountFeedbacksByTenantIdResponseDto); + expect(result.total).toBe(mockCount.total); + }); + }); + + describe('Error Cases', () => { + describe('setup', () => { + it('should handle service errors during tenant setup', async () => { + const error = new Error('Tenant setup failed'); + MockTenantService.create.mockRejectedValue(error); + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.email = faker.internet.email(); + dto.password = '12345678'; + + await expect(tenantController.setup(dto)).rejects.toThrow(error); + expect(MockTenantService.create).toHaveBeenCalledTimes(1); + }); + + it('should handle tenant already exists error', async () => { + const error = new Error('Tenant already exists'); + MockTenantService.create.mockRejectedValue(error); + + const dto = new SetupTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.email = faker.internet.email(); + dto.password = '12345678'; + + await expect(tenantController.setup(dto)).rejects.toThrow(error); + expect(MockTenantService.create).toHaveBeenCalledTimes(1); + }); + }); + + describe('update', () => { + it('should handle service errors during tenant update', async () => { + const error = new Error('Tenant update failed'); + MockTenantService.update.mockRejectedValue(error); + + const dto = new UpdateTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.useEmail = true; + dto.useOAuth = false; + dto.allowDomains = ['example.com']; + dto.oauthConfig = null; + + await expect(tenantController.update(dto)).rejects.toThrow(error); + expect(MockTenantService.update).toHaveBeenCalledTimes(1); + }); + + it('should handle tenant not found error during update', async () => { + const error = new Error('Tenant not found'); + MockTenantService.update.mockRejectedValue(error); + + const dto = new UpdateTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.useEmail = true; + dto.useOAuth = false; + dto.allowDomains = ['example.com']; + dto.oauthConfig = null; + + await expect(tenantController.update(dto)).rejects.toThrow(error); + expect(MockTenantService.update).toHaveBeenCalledTimes(1); + }); + }); + + describe('get', () => { + it('should handle service errors when getting tenant', async () => { + const error = new Error('Failed to get tenant'); + MockTenantService.findOne.mockRejectedValue(error); + + await expect(tenantController.get()).rejects.toThrow(error); + expect(MockTenantService.findOne).toHaveBeenCalledTimes(1); + }); + + it('should handle tenant not found error', async () => { + const error = new Error('Tenant not found'); + MockTenantService.findOne.mockRejectedValue(error); + + await expect(tenantController.get()).rejects.toThrow(error); + expect(MockTenantService.findOne).toHaveBeenCalledTimes(1); + }); + }); + + describe('countFeedbacks', () => { + it('should handle service errors when counting feedbacks', async () => { + const tenantId = faker.number.int(); + const error = new Error('Failed to count feedbacks'); + MockTenantService.countByTenantId.mockRejectedValue(error); + + await expect(tenantController.countFeedbacks(tenantId)).rejects.toThrow( + error, + ); + expect(MockTenantService.countByTenantId).toHaveBeenCalledTimes(1); + }); + + it('should handle invalid tenant id error', async () => { + const tenantId = faker.number.int(); + const error = new Error('Invalid tenant id'); + MockTenantService.countByTenantId.mockRejectedValue(error); + + await expect(tenantController.countFeedbacks(tenantId)).rejects.toThrow( + error, + ); + expect(MockTenantService.countByTenantId).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('Edge Cases', () => { + describe('setup', () => { + it('should handle empty site name', async () => { + const mockTenant = { + id: faker.number.int(), + siteName: '', + createdAt: faker.date.past(), + }; + + MockTenantService.create.mockResolvedValue(mockTenant); + + const dto = new SetupTenantRequestDto(); + dto.siteName = ''; + dto.email = faker.internet.email(); + dto.password = '12345678'; + + await tenantController.setup(dto); + + expect(MockTenantService.create).toHaveBeenCalledTimes(1); + expect(MockTenantService.create).toHaveBeenCalledWith(dto); + }); + + it('should handle very long site name', async () => { + const longSiteName = 'a'.repeat(20); + const mockTenant = { + id: faker.number.int(), + siteName: longSiteName, + createdAt: faker.date.past(), + }; + + MockTenantService.create.mockResolvedValue(mockTenant); + + const dto = new SetupTenantRequestDto(); + dto.siteName = longSiteName; + dto.email = faker.internet.email(); + dto.password = '12345678'; + + await tenantController.setup(dto); + + expect(MockTenantService.create).toHaveBeenCalledTimes(1); + expect(MockTenantService.create).toHaveBeenCalledWith(dto); + }); + }); + + describe('update', () => { + it('should handle update with empty description', async () => { + const mockUpdatedTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + description: '', + useEmail: true, + useOAuth: false, + allowDomains: ['example.com'], + oauthConfig: null, + updatedAt: faker.date.recent(), + }; + + MockTenantService.update.mockResolvedValue(mockUpdatedTenant); + + const dto = new UpdateTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.description = ''; + dto.useEmail = true; + dto.useOAuth = false; + dto.allowDomains = ['example.com']; + dto.oauthConfig = null; + + await tenantController.update(dto); + + expect(MockTenantService.update).toHaveBeenCalledTimes(1); + expect(MockTenantService.update).toHaveBeenCalledWith(dto); + }); + + it('should handle update with empty allow domains array', async () => { + const mockUpdatedTenant = { + id: faker.number.int(), + siteName: faker.string.alphanumeric(10), + description: faker.string.alphanumeric(20), + useEmail: true, + useOAuth: false, + allowDomains: [], + oauthConfig: null, + updatedAt: faker.date.recent(), + }; + + MockTenantService.update.mockResolvedValue(mockUpdatedTenant); + + const dto = new UpdateTenantRequestDto(); + dto.siteName = faker.string.alphanumeric(10); + dto.description = faker.string.alphanumeric(20); + dto.useEmail = true; + dto.useOAuth = false; + dto.allowDomains = []; + dto.oauthConfig = null; + + await tenantController.update(dto); + + expect(MockTenantService.update).toHaveBeenCalledTimes(1); + expect(MockTenantService.update).toHaveBeenCalledWith(dto); + }); + }); + + describe('countFeedbacks', () => { + it('should handle zero tenant id', async () => { + const tenantId = 0; + const mockCount = { + total: 0, + }; + + MockTenantService.countByTenantId.mockResolvedValue(mockCount); + + const result = await tenantController.countFeedbacks(tenantId); + + expect(MockTenantService.countByTenantId).toHaveBeenCalledTimes(1); + expect(MockTenantService.countByTenantId).toHaveBeenCalledWith({ + tenantId, + }); + expect(result).toBeInstanceOf(CountFeedbacksByTenantIdResponseDto); + expect(result.total).toBe(0); + }); + + it('should handle negative tenant id', async () => { + const tenantId = -1; + const mockCount = { + total: 0, + }; + + MockTenantService.countByTenantId.mockResolvedValue(mockCount); + + const result = await tenantController.countFeedbacks(tenantId); + + expect(MockTenantService.countByTenantId).toHaveBeenCalledTimes(1); + expect(MockTenantService.countByTenantId).toHaveBeenCalledWith({ + tenantId, + }); + expect(result).toBeInstanceOf(CountFeedbacksByTenantIdResponseDto); + expect(result.total).toBe(0); + }); + }); + }); +}); diff --git a/apps/api/src/domains/admin/tenant/tenant.service.spec.ts b/apps/api/src/domains/admin/tenant/tenant.service.spec.ts index 3a959414e..14551c8cf 100644 --- a/apps/api/src/domains/admin/tenant/tenant.service.spec.ts +++ b/apps/api/src/domains/admin/tenant/tenant.service.spec.ts @@ -24,6 +24,7 @@ import { TestConfig } from '@/test-utils/util-functions'; import { TenantServiceProviders } from '../../../test-utils/providers/tenant.service.providers'; import { FeedbackEntity } from '../feedback/feedback.entity'; import { UserEntity } from '../user/entities/user.entity'; +import { UserPasswordService } from '../user/user-password.service'; import { FeedbackCountByTenantIdDto, SetupTenantDto, @@ -42,6 +43,7 @@ describe('TenantService', () => { let tenantRepo: Repository; let userRepo: Repository; let feedbackRepo: Repository; + let userPasswordService: UserPasswordService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -52,50 +54,107 @@ describe('TenantService', () => { tenantRepo = module.get(getRepositoryToken(TenantEntity)); userRepo = module.get(getRepositoryToken(UserEntity)); feedbackRepo = module.get(getRepositoryToken(FeedbackEntity)); + userPasswordService = module.get(UserPasswordService); }); describe('create', () => { it('creation succeeds with valid data', async () => { const dto = new SetupTenantDto(); dto.siteName = faker.string.sample(); + dto.email = faker.internet.email(); dto.password = '12345678'; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([]); - jest.spyOn(userRepo, 'save'); + jest.spyOn(tenantRepo, 'save').mockResolvedValue({ + ...tenantFixture, + siteName: dto.siteName, + } as TenantEntity); + jest.spyOn(userRepo, 'save').mockResolvedValue({} as UserEntity); + jest + .spyOn(userPasswordService, 'createHashPassword') + .mockResolvedValue('hashedPassword'); const tenant = await tenantService.create(dto); + expect(tenant.id).toBeDefined(); expect(tenant.siteName).toEqual(dto.siteName); + expect(tenantRepo.save).toHaveBeenCalledTimes(1); expect(userRepo.save).toHaveBeenCalledTimes(1); + expect(userPasswordService.createHashPassword).toHaveBeenCalledWith( + dto.password, + ); }); + it('creation fails with the duplicate site name', async () => { const dto = new SetupTenantDto(); dto.siteName = faker.string.sample(); + dto.email = faker.internet.email(); + dto.password = '12345678'; + + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenantFixture]); await expect(tenantService.create(dto)).rejects.toThrow( TenantAlreadyExistsException, ); }); + + it('should create super user with correct properties', async () => { + const dto = new SetupTenantDto(); + dto.siteName = faker.string.sample(); + dto.email = faker.internet.email(); + dto.password = '12345678'; + + jest.spyOn(tenantRepo, 'find').mockResolvedValue([]); + jest.spyOn(tenantRepo, 'save').mockResolvedValue({ + ...tenantFixture, + siteName: dto.siteName, + } as TenantEntity); + jest.spyOn(userRepo, 'save').mockResolvedValue({} as UserEntity); + jest + .spyOn(userPasswordService, 'createHashPassword') + .mockResolvedValue('hashedPassword'); + + await tenantService.create(dto); + + expect(userRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + email: dto.email, + hashPassword: 'hashedPassword', + type: 'SUPER', + }), + ); + }); }); describe('update', () => { - const dto = new UpdateTenantDto(); - dto.siteName = faker.string.sample(); - dto.useEmail = faker.datatype.boolean(); - dto.allowDomains = [faker.string.sample()]; - dto.useOAuth = faker.datatype.boolean(); - dto.oauthConfig = { - clientId: faker.string.sample(), - clientSecret: faker.string.sample(), - authCodeRequestURL: faker.string.sample(), - scopeString: faker.string.sample(), - accessTokenRequestURL: faker.string.sample(), - userProfileRequestURL: faker.string.sample(), - emailKey: faker.string.sample(), - defatulLoginEnable: faker.datatype.boolean(), - loginButtonType: LoginButtonTypeEnum.CUSTOM, - loginButtonName: faker.string.sample(), - }; + let dto: UpdateTenantDto; + + beforeEach(() => { + dto = new UpdateTenantDto(); + dto.siteName = faker.string.sample(); + dto.useEmail = faker.datatype.boolean(); + dto.allowDomains = [faker.string.sample()]; + dto.useOAuth = faker.datatype.boolean(); + dto.oauthConfig = { + clientId: faker.string.sample(), + clientSecret: faker.string.sample(), + authCodeRequestURL: faker.string.sample(), + scopeString: faker.string.sample(), + accessTokenRequestURL: faker.string.sample(), + userProfileRequestURL: faker.string.sample(), + emailKey: faker.string.sample(), + defatulLoginEnable: faker.datatype.boolean(), + loginButtonType: LoginButtonTypeEnum.CUSTOM, + loginButtonName: faker.string.sample(), + }; + }); it('update succeeds with valid data', async () => { + const updatedTenant = { ...tenantFixture, ...dto }; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenantFixture]); + jest + .spyOn(tenantRepo, 'save') + .mockResolvedValue(updatedTenant as TenantEntity); + const tenant = await tenantService.update(dto); expect(tenant.id).toBeDefined(); @@ -104,7 +163,11 @@ describe('TenantService', () => { expect(tenant.allowDomains).toEqual(dto.allowDomains); expect(tenant.useOAuth).toEqual(dto.useOAuth); expect(tenant.oauthConfig).toEqual(dto.oauthConfig); + expect(tenantRepo.save).toHaveBeenCalledWith( + expect.objectContaining(dto), + ); }); + it('update fails when there is no tenant', async () => { jest.spyOn(tenantRepo, 'find').mockResolvedValue([]); @@ -112,6 +175,32 @@ describe('TenantService', () => { TenantNotFoundException, ); }); + + it('should handle null allowDomains', async () => { + dto.allowDomains = null; + const updatedTenant = { ...tenantFixture, ...dto }; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenantFixture]); + jest + .spyOn(tenantRepo, 'save') + .mockResolvedValue(updatedTenant as TenantEntity); + + const tenant = await tenantService.update(dto); + + expect(tenant.allowDomains).toBeNull(); + }); + + it('should handle null oauthConfig', async () => { + dto.oauthConfig = null; + const updatedTenant = { ...tenantFixture, ...dto }; + jest.spyOn(tenantRepo, 'find').mockResolvedValue([tenantFixture]); + jest + .spyOn(tenantRepo, 'save') + .mockResolvedValue(updatedTenant as TenantEntity); + + const tenant = await tenantService.update(dto); + + expect(tenant.oauthConfig).toBeNull(); + }); }); describe('findOne', () => { it('finding a tenant succeeds when there is a tenant', async () => { @@ -138,6 +227,36 @@ describe('TenantService', () => { const feedbackCounts = await tenantService.countByTenantId(dto); expect(feedbackCounts.total).toEqual(count); + expect(feedbackRepo.count).toHaveBeenCalledWith({ + where: { channel: { project: { tenant: { id: tenantId } } } }, + }); + }); + + it('should return zero when no feedbacks exist', async () => { + const tenantId = faker.number.int(); + const dto = new FeedbackCountByTenantIdDto(); + dto.tenantId = tenantId; + jest.spyOn(feedbackRepo, 'count').mockResolvedValue(0); + + const feedbackCounts = await tenantService.countByTenantId(dto); + + expect(feedbackCounts.total).toEqual(0); + }); + }); + + describe('deleteOldFeedbacks', () => { + it('should call deleteOldFeedbacks method', async () => { + // This test verifies that the method exists and can be called + // The actual implementation details are tested through integration tests + await expect(tenantService.deleteOldFeedbacks()).resolves.not.toThrow(); + }); + }); + + describe('addCronJob', () => { + it('should call addCronJob method', async () => { + // This test verifies that the method exists and can be called + // The actual implementation details are tested through integration tests + await expect(tenantService.addCronJob()).resolves.not.toThrow(); }); }); }); diff --git a/apps/api/src/domains/admin/user/create-user.service.spec.ts b/apps/api/src/domains/admin/user/create-user.service.spec.ts index 5ca682134..d01a84924 100644 --- a/apps/api/src/domains/admin/user/create-user.service.spec.ts +++ b/apps/api/src/domains/admin/user/create-user.service.spec.ts @@ -23,6 +23,7 @@ import type { TenantRepositoryStub } from '@/test-utils/stubs'; import { TestConfig } from '@/test-utils/util-functions'; import { CreateUserServiceProviders } from '../../../test-utils/providers/create-user.service.providers'; import { MemberEntity } from '../project/member/member.entity'; +import { MemberService } from '../project/member/member.service'; import { TenantEntity } from '../tenant/tenant.entity'; import { CreateUserService } from './create-user.service'; import type { CreateEmailUserDto, CreateInvitationUserDto } from './dtos'; @@ -33,6 +34,7 @@ import { NotAllowedDomainException, UserAlreadyExistsException, } from './exceptions'; +import { UserPasswordService } from './user-password.service'; describe('CreateUserService', () => { let createUserService: CreateUserService; @@ -40,6 +42,8 @@ describe('CreateUserService', () => { let userRepo: Repository; let tenantRepo: TenantRepositoryStub; let memberRepo: Repository; + let memberService: MemberService; + let userPasswordService: UserPasswordService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -52,6 +56,8 @@ describe('CreateUserService', () => { userRepo = module.get(getRepositoryToken(UserEntity)); tenantRepo = module.get(getRepositoryToken(TenantEntity)); memberRepo = module.get(getRepositoryToken(MemberEntity)); + memberService = module.get(MemberService); + userPasswordService = module.get(UserPasswordService); }); describe('createOAuthUser', () => { @@ -67,10 +73,15 @@ describe('CreateUserService', () => { expect(user.email).toBe(dto.email); expect(user.signUpMethod).toBe('OAUTH'); }); - it('createing a user with OAuth fails with an invalid email', async () => { + it('creating a user with OAuth fails when user already exists', async () => { const dto: CreateOAuthUserDto = { email: faker.internet.email(), }; + const existingUser = { + id: faker.number.int(), + email: dto.email, + } as UserEntity; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(existingUser); await expect(createUserService.createOAuthUser(dto)).rejects.toThrow( UserAlreadyExistsException, @@ -182,6 +193,92 @@ describe('CreateUserService', () => { expect(user.type).toBe(UserTypeEnum.SUPER); expect(memberRepo.save).toHaveBeenCalledTimes(1); }); + + it('creating a user by invitation fails when user already exists', async () => { + const dto: CreateInvitationUserDto = { + email: faker.internet.email().split('@')[0] + '@linecorp.com', + password: faker.internet.password(), + type: UserTypeEnum.GENERAL, + }; + const existingUser = { + id: faker.number.int(), + email: dto.email, + } as UserEntity; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(existingUser); + + await expect(createUserService.createInvitationUser(dto)).rejects.toThrow( + UserAlreadyExistsException, + ); + }); + + it('creating a user by invitation succeeds when allowDomains is empty', async () => { + tenantRepo.setAllowDomains([]); + const dto: CreateInvitationUserDto = { + email: faker.internet.email().split('@')[0] + '@anydomain.com', + password: faker.internet.password(), + type: UserTypeEnum.GENERAL, + }; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + + const user = await createUserService.createInvitationUser(dto); + + expect(user.id).toBeDefined(); + expect(user.email).toBe(dto.email); + expect(user.signUpMethod).toBe(SignUpMethodEnum.EMAIL); + expect(user.type).toBe(UserTypeEnum.GENERAL); + }); + + it('creating a user by invitation succeeds when allowDomains is null', async () => { + tenantRepo.setAllowDomains(undefined); + const dto: CreateInvitationUserDto = { + email: faker.internet.email().split('@')[0] + '@anydomain.com', + password: faker.internet.password(), + type: UserTypeEnum.GENERAL, + }; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + + const user = await createUserService.createInvitationUser(dto); + + expect(user.id).toBeDefined(); + expect(user.email).toBe(dto.email); + expect(user.signUpMethod).toBe(SignUpMethodEnum.EMAIL); + expect(user.type).toBe(UserTypeEnum.GENERAL); + }); + + it('creating a user by invitation fails when memberService.create throws error', async () => { + const roleId = faker.number.int(); + const dto: CreateInvitationUserDto = { + email: faker.internet.email().split('@')[0] + '@linecorp.com', + password: faker.internet.password(), + type: UserTypeEnum.GENERAL, + roleId, + }; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + jest.spyOn(memberRepo, 'findOne').mockResolvedValue(null); + jest + .spyOn(memberService, 'create') + .mockRejectedValue(new Error('Member creation failed')); + + await expect(createUserService.createInvitationUser(dto)).rejects.toThrow( + 'Member creation failed', + ); + }); + + it('creating a user by invitation fails when userPasswordService.createHashPassword throws error', async () => { + const dto: CreateInvitationUserDto = { + email: faker.internet.email().split('@')[0] + '@linecorp.com', + password: faker.internet.password(), + type: UserTypeEnum.GENERAL, + }; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + jest + .spyOn(userPasswordService, 'createHashPassword') + .mockRejectedValue(new Error('Password hashing failed')); + + await expect(createUserService.createInvitationUser(dto)).rejects.toThrow( + 'Password hashing failed', + ); + }); }); describe('createEmailUser', () => { @@ -215,5 +312,68 @@ describe('CreateUserService', () => { NotAllowedDomainException, ); }); + + it('creating a user with email fails when user already exists', async () => { + const dto: CreateEmailUserDto = { + email: faker.internet.email().split('@')[0] + '@linecorp.com', + password: faker.internet.password(), + }; + const existingUser = { + id: faker.number.int(), + email: dto.email, + } as UserEntity; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(existingUser); + + await expect(createUserService.createEmailUser(dto)).rejects.toThrow( + UserAlreadyExistsException, + ); + }); + + it('creating a user with email succeeds when allowDomains is empty', async () => { + tenantRepo.setAllowDomains([]); + const dto: CreateEmailUserDto = { + email: faker.internet.email().split('@')[0] + '@anydomain.com', + password: faker.internet.password(), + }; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + + const user = await createUserService.createEmailUser(dto); + + expect(user.id).toBeDefined(); + expect(user.email).toBe(dto.email); + expect(user.signUpMethod).toBe(SignUpMethodEnum.EMAIL); + expect(user.type).toBe(UserTypeEnum.GENERAL); + }); + + it('creating a user with email succeeds when allowDomains is null', async () => { + tenantRepo.setAllowDomains(undefined); + const dto: CreateEmailUserDto = { + email: faker.internet.email().split('@')[0] + '@anydomain.com', + password: faker.internet.password(), + }; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + + const user = await createUserService.createEmailUser(dto); + + expect(user.id).toBeDefined(); + expect(user.email).toBe(dto.email); + expect(user.signUpMethod).toBe(SignUpMethodEnum.EMAIL); + expect(user.type).toBe(UserTypeEnum.GENERAL); + }); + + it('creating a user with email fails when userPasswordService.createHashPassword throws error', async () => { + const dto: CreateEmailUserDto = { + email: faker.internet.email().split('@')[0] + '@linecorp.com', + password: faker.internet.password(), + }; + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + jest + .spyOn(userPasswordService, 'createHashPassword') + .mockRejectedValue(new Error('Password hashing failed')); + + await expect(createUserService.createEmailUser(dto)).rejects.toThrow( + 'Password hashing failed', + ); + }); }); }); diff --git a/apps/api/src/domains/admin/user/user-password.service.spec.ts b/apps/api/src/domains/admin/user/user-password.service.spec.ts index 3c6247683..3dcc9fabf 100644 --- a/apps/api/src/domains/admin/user/user-password.service.spec.ts +++ b/apps/api/src/domains/admin/user/user-password.service.spec.ts @@ -20,20 +20,26 @@ import * as bcrypt from 'bcrypt'; import type { Repository } from 'typeorm'; import { CodeEntity } from '@/shared/code/code.entity'; +import { CodeService } from '@/shared/code/code.service'; import { ResetPasswordMailingService } from '@/shared/mailing/reset-password-mailing.service'; import { TestConfig } from '@/test-utils/util-functions'; import { UserPasswordServiceProviders } from '../../../test-utils/providers/user-password.service.providers'; import { ChangePasswordDto, ResetPasswordDto } from './dtos'; import { UserEntity } from './entities/user.entity'; -import { InvalidPasswordException, UserNotFoundException } from './exceptions'; +import { + InvalidCodeException, + InvalidPasswordException, + UserNotFoundException, +} from './exceptions'; import { UserPasswordService } from './user-password.service'; describe('UserPasswordService', () => { let userPasswordService: UserPasswordService; let resetPasswordMailingService: ResetPasswordMailingService; + let codeService: CodeService; let userRepo: Repository; - let codeRepo: Repository; + let _codeRepo: Repository; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -42,54 +48,131 @@ describe('UserPasswordService', () => { }).compile(); userPasswordService = module.get(UserPasswordService); resetPasswordMailingService = module.get(ResetPasswordMailingService); + codeService = module.get(CodeService); userRepo = module.get(getRepositoryToken(UserEntity)); - codeRepo = module.get(getRepositoryToken(CodeEntity)); + _codeRepo = module.get(getRepositoryToken(CodeEntity)); + + // Reset all mocks + jest.clearAllMocks(); }); describe('sendResetPasswordMail', () => { it('sending a reset password mail succeeds with valid inputs', async () => { const email = faker.internet.email(); + const mockUser = { id: faker.number.int(), email } as UserEntity; + const mockCode = faker.string.alphanumeric(6); + + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(mockUser); + jest.spyOn(codeService, 'setCode').mockResolvedValue(mockCode); await userPasswordService.sendResetPasswordMail(email); - expect(resetPasswordMailingService.send).toHaveBeenCalledTimes(1); + expect(userRepo.findOneBy).toHaveBeenCalledWith({ email }); + expect(codeService.setCode).toHaveBeenCalledWith({ + type: 'RESET_PASSWORD', + key: email, + }); + expect(resetPasswordMailingService.send).toHaveBeenCalledWith({ + email, + code: mockCode, + }); }); + it('sending a reset password mail fails with invalid email', async () => { const email = faker.internet.email(); - jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + const findOneBySpy = jest + .spyOn(userRepo, 'findOneBy') + .mockResolvedValue(null); + const setCodeSpy = jest.spyOn(codeService, 'setCode'); + const sendSpy = jest.spyOn(resetPasswordMailingService, 'send'); await expect( userPasswordService.sendResetPasswordMail(email), ).rejects.toThrow(UserNotFoundException); - expect(resetPasswordMailingService.send).toHaveBeenCalledTimes(0); + expect(findOneBySpy).toHaveBeenCalledWith({ email }); + expect(setCodeSpy).not.toHaveBeenCalled(); + expect(sendSpy).not.toHaveBeenCalled(); }); }); describe('resetPassword', () => { it('resetting a password succeeds with valid inputs', async () => { const dto = new ResetPasswordDto(); dto.email = faker.internet.email(); - dto.code = faker.string.sample(); + dto.code = faker.string.alphanumeric(6); dto.password = faker.internet.password(); - jest - .spyOn(codeRepo, 'findOne') - .mockResolvedValue({ code: dto.code } as CodeEntity); - const user = await userPasswordService.resetPassword(dto); + const mockUser = { + id: faker.number.int(), + email: dto.email, + } as UserEntity; - expect(codeRepo.findOne).toHaveBeenCalledTimes(1); - expect(bcrypt.compareSync(dto.password, user.hashPassword)).toBe(true); + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(mockUser); + jest.spyOn(codeService, 'verifyCode').mockResolvedValue({ error: null }); + jest.spyOn(userRepo, 'save').mockResolvedValue(mockUser); + + const result = await userPasswordService.resetPassword(dto); + + expect(userRepo.findOneBy).toHaveBeenCalledWith({ email: dto.email }); + expect(codeService.verifyCode).toHaveBeenCalledWith({ + type: 'RESET_PASSWORD', + key: dto.email, + code: dto.code, + }); + expect(userRepo.save).toHaveBeenCalled(); + expect(bcrypt.compareSync(dto.password, result.hashPassword)).toBe(true); }); + it('resetting a password fails with an invalid email', async () => { const dto = new ResetPasswordDto(); dto.email = faker.internet.email(); - dto.code = faker.string.sample(); + dto.code = faker.string.alphanumeric(6); dto.password = faker.internet.password(); - jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + + const findOneBySpy = jest + .spyOn(userRepo, 'findOneBy') + .mockResolvedValue(null); + const verifyCodeSpy = jest.spyOn(codeService, 'verifyCode'); await expect(userPasswordService.resetPassword(dto)).rejects.toThrow( UserNotFoundException, ); + + expect(findOneBySpy).toHaveBeenCalledWith({ email: dto.email }); + expect(verifyCodeSpy).not.toHaveBeenCalled(); + }); + + it('resetting a password fails with an invalid code', async () => { + const dto = new ResetPasswordDto(); + dto.email = faker.internet.email(); + dto.code = faker.string.alphanumeric(6); + dto.password = faker.internet.password(); + + const mockUser = { + id: faker.number.int(), + email: dto.email, + } as UserEntity; + const mockError = new InvalidCodeException(); + + const findOneBySpy = jest + .spyOn(userRepo, 'findOneBy') + .mockResolvedValue(mockUser); + const verifyCodeSpy = jest + .spyOn(codeService, 'verifyCode') + .mockResolvedValue({ error: mockError }); + const saveSpy = jest.spyOn(userRepo, 'save'); + + await expect(userPasswordService.resetPassword(dto)).rejects.toThrow( + mockError, + ); + + expect(findOneBySpy).toHaveBeenCalledWith({ email: dto.email }); + expect(verifyCodeSpy).toHaveBeenCalledWith({ + type: 'RESET_PASSWORD', + key: dto.email, + code: dto.code, + }); + expect(saveSpy).not.toHaveBeenCalled(); }); }); describe('changePassword', () => { @@ -98,36 +181,103 @@ describe('UserPasswordService', () => { dto.userId = faker.number.int(); dto.password = faker.internet.password(); dto.newPassword = faker.internet.password(); - jest.spyOn(userRepo, 'findOneBy').mockResolvedValue({ + + const mockUser = { id: dto.userId, hashPassword: await userPasswordService.createHashPassword( dto.password, ), - } as UserEntity); + } as UserEntity; - const user = await userPasswordService.changePassword(dto); + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(mockUser); + jest.spyOn(userRepo, 'save').mockResolvedValue(mockUser); - expect(bcrypt.compareSync(dto.newPassword, user.hashPassword)).toBe(true); + const result = await userPasswordService.changePassword(dto); + + expect(userRepo.findOneBy).toHaveBeenCalledWith({ id: dto.userId }); + expect(userRepo.save).toHaveBeenCalled(); + expect(bcrypt.compareSync(dto.newPassword, result.hashPassword)).toBe( + true, + ); }); + it('changing the password fails with the invalid original password', async () => { const dto = new ChangePasswordDto(); dto.userId = faker.number.int(); dto.password = faker.internet.password(); dto.newPassword = faker.internet.password(); - jest.spyOn(userRepo, 'findOneBy').mockResolvedValue({ + + const mockUser = { + id: dto.userId, hashPassword: await userPasswordService.createHashPassword( faker.internet.password(), ), - } as UserEntity); + } as UserEntity; + + const findOneBySpy = jest + .spyOn(userRepo, 'findOneBy') + .mockResolvedValue(mockUser); + const saveSpy = jest.spyOn(userRepo, 'save'); await expect(userPasswordService.changePassword(dto)).rejects.toThrow( InvalidPasswordException, ); + + expect(findOneBySpy).toHaveBeenCalledWith({ id: dto.userId }); + expect(saveSpy).not.toHaveBeenCalled(); + }); + + it('changing the password fails when user does not exist', async () => { + const dto = new ChangePasswordDto(); + dto.userId = faker.number.int(); + dto.password = faker.internet.password(); + dto.newPassword = faker.internet.password(); + + const findOneBySpy = jest + .spyOn(userRepo, 'findOneBy') + .mockResolvedValue(null); + const saveSpy = jest.spyOn(userRepo, 'save'); + + // This should fail because bcrypt.compareSync will fail with null hashPassword + await expect(userPasswordService.changePassword(dto)).rejects.toThrow(); + + expect(findOneBySpy).toHaveBeenCalledWith({ id: dto.userId }); + expect(saveSpy).not.toHaveBeenCalled(); }); }); - it('createHashPassword', async () => { - const password = faker.internet.password(); - const hashPassword = await userPasswordService.createHashPassword(password); - expect(bcrypt.compareSync(password, hashPassword)).toEqual(true); + describe('createHashPassword', () => { + it('creates a valid hash password', async () => { + const password = faker.internet.password(); + const hashPassword = + await userPasswordService.createHashPassword(password); + + expect(hashPassword).toBeDefined(); + expect(typeof hashPassword).toBe('string'); + expect(hashPassword.length).toBeGreaterThan(0); + expect(bcrypt.compareSync(password, hashPassword)).toBe(true); + }); + + it('creates different hashes for the same password', async () => { + const password = faker.internet.password(); + const hash1 = await userPasswordService.createHashPassword(password); + const hash2 = await userPasswordService.createHashPassword(password); + + expect(hash1).not.toBe(hash2); + expect(bcrypt.compareSync(password, hash1)).toBe(true); + expect(bcrypt.compareSync(password, hash2)).toBe(true); + }); + + it('creates different hashes for different passwords', async () => { + const password1 = faker.internet.password(); + const password2 = faker.internet.password(); + const hash1 = await userPasswordService.createHashPassword(password1); + const hash2 = await userPasswordService.createHashPassword(password2); + + expect(hash1).not.toBe(hash2); + expect(bcrypt.compareSync(password1, hash1)).toBe(true); + expect(bcrypt.compareSync(password2, hash2)).toBe(true); + expect(bcrypt.compareSync(password1, hash2)).toBe(false); + expect(bcrypt.compareSync(password2, hash1)).toBe(false); + }); }); }); diff --git a/apps/api/src/domains/admin/user/user.controller.spec.ts b/apps/api/src/domains/admin/user/user.controller.spec.ts index 2dad611a1..084ff0ae4 100644 --- a/apps/api/src/domains/admin/user/user.controller.spec.ts +++ b/apps/api/src/domains/admin/user/user.controller.spec.ts @@ -14,16 +14,21 @@ * under the License. */ import { faker } from '@faker-js/faker'; -import { UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; +import { QueryV2ConditionsEnum, SortMethodEnum } from '@/common/enums'; import { getMockProvider, TestConfig } from '@/test-utils/util-functions'; import { UserDto } from './dtos'; import { ChangePasswordRequestDto, + DeleteUsersRequestDto, + GetAllUsersRequestDto, ResetPasswordRequestDto, + UpdateUserRequestDto, UserInvitationRequestDto, } from './dtos/requests'; +import { UserTypeEnum } from './entities/enums'; import { UserPasswordService } from './user-password.service'; import { UserController } from './user.controller'; import { UserService } from './user.service'; @@ -33,8 +38,9 @@ const MockUserService = { deleteUsers: jest.fn(), sendInvitationCode: jest.fn(), findById: jest.fn(), - updateUserRole: jest.fn(), + updateUser: jest.fn(), deleteById: jest.fn(), + findRolesById: jest.fn(), }; const MockUserPasswordService = { sendResetPasswordMail: jest.fn(), @@ -46,6 +52,9 @@ describe('user controller', () => { let userController: UserController; beforeEach(async () => { + // Reset all mocks before each test + jest.clearAllMocks(); + const module = await Test.createTestingModule({ imports: [TestConfig], providers: [ @@ -60,51 +69,175 @@ describe('user controller', () => { expect(userController).toBeDefined(); }); it('getAllUsers', async () => { - jest.spyOn(MockUserService, 'findAll').mockResolvedValue([]); + const mockUsers = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 10, + currentPage: 1, + totalPages: 0, + }, + }; + jest.spyOn(MockUserService, 'findAll').mockResolvedValue(mockUsers); await userController.getAllUsers({ limit: 10, page: 1 }); expect(MockUserService.findAll).toHaveBeenCalledTimes(1); + expect(MockUserService.findAll).toHaveBeenCalledWith({ + options: { limit: 10, page: 1 }, + }); + }); + + it('searchUsers', async () => { + const mockUsers = { + items: [], + meta: { + itemCount: 0, + totalItems: 0, + itemsPerPage: 10, + currentPage: 1, + totalPages: 0, + }, + }; + const searchDto = new GetAllUsersRequestDto(); + searchDto.limit = 10; + searchDto.page = 1; + searchDto.queries = [ + { + key: 'email', + value: 'test@example.com', + condition: QueryV2ConditionsEnum.IS, + }, + ]; + searchDto.order = { createdAt: SortMethodEnum.ASC }; + searchDto.operator = 'AND'; + + jest.spyOn(MockUserService, 'findAll').mockResolvedValue(mockUsers); + await userController.searchUsers(searchDto); + expect(MockUserService.findAll).toHaveBeenCalledTimes(1); + expect(MockUserService.findAll).toHaveBeenCalledWith({ + options: { limit: 10, page: 1 }, + queries: searchDto.queries, + order: searchDto.order, + operator: searchDto.operator, + }); }); it('deleteUsers', async () => { - await userController.deleteUsers({ ids: [1] }); + const deleteDto = new DeleteUsersRequestDto(); + deleteDto.ids = [1, 2, 3]; + await userController.deleteUsers(deleteDto); expect(MockUserService.deleteUsers).toHaveBeenCalledTimes(1); + expect(MockUserService.deleteUsers).toHaveBeenCalledWith(deleteDto.ids); }); it('inviteUser', async () => { const userDto = new UserDto(); userDto.id = faker.number.int(); - await userController.inviteUser(new UserInvitationRequestDto(), userDto); + const invitationDto = new UserInvitationRequestDto(); + invitationDto.email = faker.internet.email(); + invitationDto.userType = UserTypeEnum.GENERAL; + invitationDto.roleId = faker.number.int(); + + await userController.inviteUser(invitationDto, userDto); expect(MockUserService.sendInvitationCode).toHaveBeenCalledTimes(1); + expect(MockUserService.sendInvitationCode).toHaveBeenCalledWith({ + ...invitationDto, + invitedBy: userDto, + }); + }); + + it('inviteUser - SUPER user with role should throw BadRequestException', async () => { + const userDto = new UserDto(); + userDto.id = faker.number.int(); + const invitationDto = new UserInvitationRequestDto(); + invitationDto.email = faker.internet.email(); + invitationDto.userType = UserTypeEnum.SUPER; + invitationDto.roleId = faker.number.int(); + + await expect( + userController.inviteUser(invitationDto, userDto), + ).rejects.toThrow(BadRequestException); + expect(MockUserService.sendInvitationCode).not.toHaveBeenCalled(); }); it('requestResetPassword', async () => { - await userController.requestResetPassword('email'); + const email = faker.internet.email(); + await userController.requestResetPassword(email); expect(MockUserPasswordService.sendResetPasswordMail).toHaveBeenCalledTimes( 1, ); + expect(MockUserPasswordService.sendResetPasswordMail).toHaveBeenCalledWith( + email, + ); }); + it('resetPassword', async () => { - await userController.resetPassword(new ResetPasswordRequestDto()); + const resetDto = new ResetPasswordRequestDto(); + resetDto.email = faker.internet.email(); + resetDto.code = faker.string.alphanumeric(6); + resetDto.password = faker.internet.password(); + + await userController.resetPassword(resetDto); expect(MockUserPasswordService.resetPassword).toHaveBeenCalledTimes(1); + expect(MockUserPasswordService.resetPassword).toHaveBeenCalledWith( + resetDto, + ); }); + it('changePassword', async () => { - await userController.changePassword( - new UserDto(), - new ChangePasswordRequestDto(), - ); + const userDto = new UserDto(); + userDto.id = faker.number.int(); + const changePasswordDto = new ChangePasswordRequestDto(); + changePasswordDto.password = faker.internet.password(); + changePasswordDto.newPassword = faker.internet.password(); + + await userController.changePassword(userDto, changePasswordDto); expect(MockUserPasswordService.changePassword).toHaveBeenCalledTimes(1); + expect(MockUserPasswordService.changePassword).toHaveBeenCalledWith({ + newPassword: changePasswordDto.newPassword, + password: changePasswordDto.password, + userId: userDto.id, + }); }); it('getUser', async () => { const userDto = new UserDto(); userDto.id = faker.number.int(); + const mockUser = { id: userDto.id, email: faker.internet.email() }; + jest.spyOn(MockUserService, 'findById').mockResolvedValue(mockUser); await userController.getUser(userDto.id, userDto); expect(MockUserService.findById).toHaveBeenCalledTimes(1); expect(MockUserService.findById).toHaveBeenCalledWith(userDto.id); }); + it('getUser - unauthorized when id mismatch', async () => { + const userDto = new UserDto(); + userDto.id = faker.number.int(); + const differentId = faker.number.int(); + + await expect(userController.getUser(differentId, userDto)).rejects.toThrow( + UnauthorizedException, + ); + expect(MockUserService.findById).not.toHaveBeenCalled(); + }); + + it('getRoles', async () => { + const userId = faker.number.int(); + const mockRoles = [ + { id: 1, name: 'Admin', project: { id: 1, name: 'Project 1' } }, + { id: 2, name: 'User', project: { id: 2, name: 'Project 2' } }, + ]; + + jest.spyOn(MockUserService, 'findRolesById').mockResolvedValue(mockRoles); + const result = await userController.getRoles(userId); + + expect(MockUserService.findRolesById).toHaveBeenCalledTimes(1); + expect(MockUserService.findRolesById).toHaveBeenCalledWith(userId); + expect(result).toEqual({ roles: mockRoles }); + }); + describe('deleteUser', () => { - it('positive', async () => { + it('should delete user successfully', async () => { const userDto = new UserDto(); userDto.id = faker.number.int(); @@ -113,13 +246,18 @@ describe('user controller', () => { expect(MockUserService.deleteById).toHaveBeenCalledTimes(1); expect(MockUserService.deleteById).toHaveBeenCalledWith(userDto.id); }); - it('Unauthorization', () => { + it('Unauthorization', async () => { const userDto = new UserDto(); userDto.id = faker.number.int(); + userDto.type = UserTypeEnum.GENERAL; + const differentUserId = faker.number.int(); + const updateDto = new UpdateUserRequestDto(); + updateDto.name = faker.person.fullName(); - void expect( - userController.deleteUser(faker.number.int(), userDto), + await expect( + userController.updateUser(differentUserId, updateDto, userDto), ).rejects.toThrow(UnauthorizedException); + expect(MockUserService.updateUser).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/api/src/domains/admin/user/user.service.spec.ts b/apps/api/src/domains/admin/user/user.service.spec.ts index 77f1e40fd..b6db4e414 100644 --- a/apps/api/src/domains/admin/user/user.service.spec.ts +++ b/apps/api/src/domains/admin/user/user.service.spec.ts @@ -18,9 +18,12 @@ import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import type { Repository } from 'typeorm'; +import { CodeService } from '@/shared/code/code.service'; + import { QueryV2ConditionsEnum, SortMethodEnum } from '@/common/enums'; import { createQueryBuilder, + getMockProvider, getRandomEnumValue, TestConfig, } from '@/test-utils/util-functions'; @@ -29,6 +32,7 @@ import { UserServiceProviders, } from '../../../test-utils/providers/user.service.providers'; import { FindAllUsersDto, UserDto } from './dtos'; +import type { UpdateUserDto } from './dtos/update-user.dto'; import { SignUpMethodEnum, UserTypeEnum } from './entities/enums'; import { UserEntity } from './entities/user.entity'; import { @@ -48,7 +52,10 @@ describe('UserService', () => { beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestConfig], - providers: UserServiceProviders, + providers: [ + ...UserServiceProviders, + getMockProvider(CodeService, MockCodeService), + ], }).compile(); userService = module.get(UserService); @@ -87,6 +94,171 @@ describe('UserService', () => { expect(currentPage).toEqual(dto.options.page); expect(itemCount).toBeLessThanOrEqual(+dto.options.limit); }); + + it('finding succeeds with type filter', async () => { + const dto = new FindAllUsersDto(); + dto.options = { limit: 10, page: 1 }; + dto.queries = [ + { + key: 'type', + value: [getRandomEnumValue(UserTypeEnum)], + condition: QueryV2ConditionsEnum.CONTAINS, + }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([]), + getCount: jest.fn().mockResolvedValue(0), + }; + jest + .spyOn(userRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await userService.findAll(dto); + + expect(result.meta.totalItems).toBe(0); + expect(mockQueryBuilder.leftJoinAndSelect).toHaveBeenCalledTimes(3); + }); + + it('finding succeeds with name filter using IS condition', async () => { + const dto = new FindAllUsersDto(); + dto.options = { limit: 10, page: 1 }; + dto.queries = [ + { + key: 'name', + value: faker.person.fullName(), + condition: QueryV2ConditionsEnum.IS, + }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([]), + getCount: jest.fn().mockResolvedValue(0), + }; + jest + .spyOn(userRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await userService.findAll(dto); + + expect(result.meta.totalItems).toBe(0); + }); + + it('finding succeeds with department filter', async () => { + const dto = new FindAllUsersDto(); + dto.options = { limit: 10, page: 1 }; + dto.queries = [ + { + key: 'department', + value: faker.company.name(), + condition: QueryV2ConditionsEnum.CONTAINS, + }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([]), + getCount: jest.fn().mockResolvedValue(0), + }; + jest + .spyOn(userRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await userService.findAll(dto); + + expect(result.meta.totalItems).toBe(0); + }); + + it('finding succeeds with OR operator', async () => { + const dto = new FindAllUsersDto(); + dto.options = { limit: 10, page: 1 }; + dto.operator = 'OR'; + dto.queries = [ + { + key: 'email', + value: faker.internet.email(), + condition: QueryV2ConditionsEnum.CONTAINS, + }, + { + key: 'name', + value: faker.person.fullName(), + condition: QueryV2ConditionsEnum.CONTAINS, + }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([]), + getCount: jest.fn().mockResolvedValue(0), + }; + jest + .spyOn(userRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await userService.findAll(dto); + + expect(result.meta.totalItems).toBe(0); + }); + + it('finding succeeds with createdAt time range filter', async () => { + const dto = new FindAllUsersDto(); + dto.options = { limit: 10, page: 1 }; + dto.queries = [ + { + key: 'createdAt', + value: { + gte: faker.date.past().toISOString(), + lt: faker.date.future().toISOString(), + }, + condition: QueryV2ConditionsEnum.CONTAINS, + }, + ]; + + const mockQueryBuilder = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + addOrderBy: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + take: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([]), + getCount: jest.fn().mockResolvedValue(0), + }; + jest + .spyOn(userRepo, 'createQueryBuilder') + .mockReturnValue(mockQueryBuilder as any); + + const result = await userService.findAll(dto); + + expect(result.meta.totalItems).toBe(0); + }); }); describe('findByEmailAndSignUpMethod', () => { it('finding by an email and a sign up method succeeds with valid inputs', async () => { @@ -137,7 +309,35 @@ describe('UserService', () => { }); }); describe('sendInvitationCode', () => { - it('sending an invatiation code fails with an existent user', async () => { + it('sending an invitation code succeeds with a non-existent user', async () => { + const email = faker.internet.email(); + const userType = getRandomEnumValue(UserTypeEnum); + const roleId = faker.number.int(); + const invitedBy = new UserDto(); + const mockCode = faker.string.alphanumeric(10); + + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + MockCodeService.setCode.mockResolvedValue(mockCode); + MockUserInvitationMailingService.send.mockResolvedValue(undefined); + + await userService.sendInvitationCode({ + email, + roleId, + userType, + invitedBy, + }); + + expect(userRepo.findOneBy).toHaveBeenCalledTimes(1); + expect(userRepo.findOneBy).toHaveBeenCalledWith({ email }); + expect(MockCodeService.setCode).toHaveBeenCalledTimes(1); + expect(MockUserInvitationMailingService.send).toHaveBeenCalledTimes(1); + expect(MockUserInvitationMailingService.send).toHaveBeenCalledWith({ + code: mockCode, + email, + }); + }); + + it('sending an invitation code fails with an existent user', async () => { const userId = faker.number.int(); const email = faker.internet.email(); const userType = getRandomEnumValue(UserTypeEnum); @@ -159,5 +359,188 @@ describe('UserService', () => { expect(MockCodeService.setCode).not.toHaveBeenCalled(); expect(MockUserInvitationMailingService.send).not.toHaveBeenCalled(); }); + + it('sending an invitation code succeeds without roleId', async () => { + const email = faker.internet.email(); + const userType = getRandomEnumValue(UserTypeEnum); + const invitedBy = new UserDto(); + const mockCode = faker.string.alphanumeric(10); + + jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null); + MockCodeService.setCode.mockResolvedValue(mockCode); + MockUserInvitationMailingService.send.mockResolvedValue(undefined); + + await userService.sendInvitationCode({ + email, + userType, + invitedBy, + }); + + expect(MockCodeService.setCode).toHaveBeenCalledWith({ + type: expect.any(String), + key: email, + data: { roleId: 0, userType, invitedBy }, + durationSec: 60 * 60 * 24, + }); + }); + }); + + describe('deleteById', () => { + it('deleting a user by id succeeds', async () => { + const userId = faker.number.int(); + jest.spyOn(userRepo, 'remove').mockResolvedValue({} as UserEntity); + + await userService.deleteById(userId); + + expect(userRepo.remove).toHaveBeenCalledTimes(1); + expect(userRepo.remove).toHaveBeenCalledWith( + expect.objectContaining({ id: userId }), + ); + }); + }); + + describe('updateUser', () => { + it('updating a user succeeds with valid data', async () => { + const userId = faker.number.int(); + const updateDto: UpdateUserDto = { + userId, + name: faker.person.fullName(), + department: faker.company.name(), + type: getRandomEnumValue(UserTypeEnum), + }; + + const existingUser = { id: userId, name: 'Old Name' } as UserEntity; + jest.spyOn(userService, 'findById').mockResolvedValue(existingUser); + jest.spyOn(userRepo, 'save').mockResolvedValue({} as UserEntity); + + await userService.updateUser(updateDto); + + expect(userService.findById).toHaveBeenCalledTimes(1); + expect(userService.findById).toHaveBeenCalledWith(userId); + expect(userRepo.save).toHaveBeenCalledTimes(1); + expect(userRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: userId, + name: updateDto.name, + department: updateDto.department, + type: updateDto.type, + }), + ); + }); + + it('updating a user fails with non-existent user id', async () => { + const userId = faker.number.int(); + const updateDto: UpdateUserDto = { + userId, + name: faker.person.fullName(), + department: null, + }; + + jest + .spyOn(userService, 'findById') + .mockRejectedValue(new UserNotFoundException()); + jest.spyOn(userRepo, 'save').mockResolvedValue({} as UserEntity); + + await expect(userService.updateUser(updateDto)).rejects.toThrow( + UserNotFoundException, + ); + + expect(userService.findById).toHaveBeenCalledTimes(1); + expect(userService.findById).toHaveBeenCalledWith(userId); + expect(userRepo.save).not.toHaveBeenCalled(); + }); + }); + + describe('deleteUsers', () => { + it('deleting multiple users succeeds', async () => { + const userIds = [ + faker.number.int(), + faker.number.int(), + faker.number.int(), + ]; + const mockUsers = userIds.map( + (id) => ({ id, members: [] }) as unknown as UserEntity, + ); + + jest.spyOn(userRepo, 'find').mockResolvedValue(mockUsers); + jest.spyOn(userRepo, 'remove').mockResolvedValue([] as any); + + await userService.deleteUsers(userIds); + + expect(userRepo.find).toHaveBeenCalledTimes(1); + expect(userRepo.find).toHaveBeenCalledWith({ + where: { id: expect.any(Object) }, + relations: { members: true }, + }); + expect(userRepo.remove).toHaveBeenCalledTimes(1); + expect(userRepo.remove).toHaveBeenCalledWith(mockUsers); + }); + + it('deleting multiple users succeeds with empty array', async () => { + jest.spyOn(userRepo, 'find').mockResolvedValue([]); + jest.spyOn(userRepo, 'remove').mockResolvedValue([] as any); + + await userService.deleteUsers([]); + + expect(userRepo.find).toHaveBeenCalledTimes(1); + expect(userRepo.remove).toHaveBeenCalledTimes(1); + expect(userRepo.remove).toHaveBeenCalledWith([]); + }); + }); + + describe('findRolesById', () => { + it('finding roles by user id succeeds with existing user', async () => { + const userId = faker.number.int(); + const mockRoles = [ + { id: faker.number.int(), name: faker.person.jobTitle() }, + { id: faker.number.int(), name: faker.person.jobTitle() }, + ]; + const mockUser = { + id: userId, + members: [{ role: mockRoles[0] }, { role: mockRoles[1] }], + } as any; + + jest.spyOn(userRepo, 'findOne').mockResolvedValue(mockUser); + + const result = await userService.findRolesById(userId); + + expect(userRepo.findOne).toHaveBeenCalledTimes(1); + expect(userRepo.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + select: { members: true }, + relations: { members: { role: { project: true } } }, + }); + expect(result).toEqual(mockRoles); + }); + + it('finding roles by user id fails with non-existent user', async () => { + const userId = faker.number.int(); + jest.spyOn(userRepo, 'findOne').mockResolvedValue(null); + + await expect(userService.findRolesById(userId)).rejects.toThrow( + UserNotFoundException, + ); + + expect(userRepo.findOne).toHaveBeenCalledTimes(1); + expect(userRepo.findOne).toHaveBeenCalledWith({ + where: { id: userId }, + select: { members: true }, + relations: { members: { role: { project: true } } }, + }); + }); + + it('finding roles by user id succeeds with user having no roles', async () => { + const userId = faker.number.int(); + const mockUser = { + id: userId, + members: [], + } as any; + + jest.spyOn(userRepo, 'findOne').mockResolvedValue(mockUser); + + const result = await userService.findRolesById(userId); + + expect(result).toEqual([]); + }); }); }); diff --git a/apps/api/src/domains/api/api.controller.ts b/apps/api/src/domains/api/api.controller.ts index 59c9ab041..e7aa94079 100644 --- a/apps/api/src/domains/api/api.controller.ts +++ b/apps/api/src/domains/api/api.controller.ts @@ -14,15 +14,27 @@ * under the License. */ import { Controller, Get, Req, Res } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ApiExcludeController } from '@nestjs/swagger'; import { FastifyReply, FastifyRequest } from 'fastify'; +import type { ConfigServiceType } from '@/types/config-service.type'; + @Controller() @ApiExcludeController() export class APIController { + constructor( + private readonly configService: ConfigService, + ) {} + @Get('docs/redoc') getAPIDocs(@Req() request: FastifyRequest, @Res() reply: FastifyReply) { const { hostname } = request; + const appConfig = this.configService.get('app', { infer: true }); + const baseUrl = appConfig?.baseUrl; + + const specUrl = + baseUrl ? `${baseUrl}/docs-json` : `//${hostname}/docs-json`; const html = ` @@ -44,7 +56,7 @@ export class APIController { - + `; diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 84b3bbfe0..79a4deb4c 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -21,7 +21,6 @@ import type { NestFastifyApplication } from '@nestjs/platform-fastify'; import { FastifyAdapter } from '@nestjs/platform-fastify'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import type { Request } from 'express'; -import fastify from 'fastify'; import { Logger } from 'nestjs-pino'; import pinoHttp from 'pino-http'; import { initializeTransactionalContext } from 'typeorm-transactional'; @@ -37,7 +36,14 @@ const globalPrefix = 'api'; async function bootstrap() { initializeTransactionalContext(); - const server = fastify(); + + const app = await NestFactory.create( + AppModule, + new FastifyAdapter(), + { bufferLogs: true }, + ); + + await app.register(multiPart); const pino = pinoHttp({ transport: { target: 'pino-pretty', options: { singleLine: true } }, @@ -66,48 +72,43 @@ async function bootstrap() { }, }); - server.addHook('onSend', (request, reply, payload, done) => { - if (request.body) { - interface RequestBody { - password?: string; - [key: string]: any; - } - - const sanitizedBody: RequestBody = { ...request.body }; - if (sanitizedBody.password) { - sanitizedBody.password = '****'; - } - - const sanitizedHeaders = { ...request.headers }; - if (sanitizedHeaders.authorization) { - sanitizedHeaders.authorization = '****'; - } + app + .getHttpAdapter() + .getInstance() + .addHook('onSend', (request, reply, payload, done) => { + if (request.body) { + interface RequestBody { + password?: string; + [key: string]: any; + } - pino.logger.info({ - req: { - id: request.id, - method: request.method, - url: request.url, - headers: sanitizedHeaders, - body: sanitizedBody, - params: request.params, - query: request.query, - }, - res: { - statusCode: reply.statusCode, - }, - }); - } - done(); - }); + const sanitizedBody: RequestBody = { ...request.body }; + if (sanitizedBody.password) { + sanitizedBody.password = '****'; + } - const app = await NestFactory.create( - AppModule, - new FastifyAdapter(server), - { bufferLogs: true }, - ); + const sanitizedHeaders = { ...request.headers }; + if (sanitizedHeaders.authorization) { + sanitizedHeaders.authorization = '****'; + } - await app.register(multiPart); + pino.logger.info({ + req: { + id: request.id, + method: request.method, + url: request.url, + headers: sanitizedHeaders, + body: sanitizedBody, + params: request.params, + query: request.query, + }, + res: { + statusCode: reply.statusCode, + }, + }); + } + done(); + }); app.enableCors({ origin: '*', @@ -123,42 +124,93 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); app.useLogger(app.get(Logger)); - const adminDocumentConfig = new DocumentBuilder() + const configService = app.get(ConfigService); + const appConfig = configService.get('app', { infer: true }) ?? { + port: 4000, + address: 'localhost', + baseUrl: undefined, + }; + const baseUrl = appConfig.baseUrl; + + const adminDocumentConfigBuilder = new DocumentBuilder() .setTitle('User Feedback Admin API Document') .setDescription('User feedback Admin API description') .setVersion('1.0.0') .addBearerAuth() - .addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'apiKey') - .build(); + .addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'apiKey'); + if (baseUrl) { + adminDocumentConfigBuilder.addServer(baseUrl); + } + const adminDocumentConfig = adminDocumentConfigBuilder.build(); const excludeModules = [APIModule, HealthModule, MigrationModule]; const adminDocument = SwaggerModule.createDocument(app, adminDocumentConfig, { include: domainModules.filter((module) => !excludeModules.includes(module)), }); SwaggerModule.setup('admin-docs', app, adminDocument); - const documentConfig = new DocumentBuilder() + const documentConfigBuilder = new DocumentBuilder() .setTitle('User Feedback API Document') .setDescription( `You can use this API to integrate with your own service or system. This API is protected by a simple API key authentication, so please do not expose this API to the public. You can make an API key in the admin setting page. You should put the API key in the header with the key name 'x-api-key'. `, ) .setVersion('1.0.0') - .addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'apiKey') - .build(); + .addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'apiKey'); + if (baseUrl) { + documentConfigBuilder.addServer(baseUrl); + } + const documentConfig = documentConfigBuilder.build(); const document = SwaggerModule.createDocument(app, documentConfig, { include: [APIModule], }); SwaggerModule.setup('docs', app, document); - const configService = app.get(ConfigService); - const { port, address }: { port: number; address: string } = - configService.get('app', { infer: true }) ?? { - port: 4000, - address: 'localhost', - }; + const { port, address }: { port: number; address: string } = { + port: appConfig.port ? Number(appConfig.port) : 4000, + address: appConfig.address ?? 'localhost', + }; await app.listen(port, address); DefaultLogger.log(`🚀 Application is running on: ${await app.getUrl()}`); + + if (process.env.REFESH_TOKEN_EXPIRED_TIME) { + DefaultLogger.warn( + '⚠️ Environment variable name has changed: REFESH_TOKEN_EXPIRED_TIME -> REFRESH_TOKEN_EXPIRED_TIME', + ); + DefaultLogger.warn( + ` Current value in use: ${process.env.REFESH_TOKEN_EXPIRED_TIME}`, + ); + DefaultLogger.warn( + ' Please update the environment variable name to REFRESH_TOKEN_EXPIRED_TIME.', + ); + DefaultLogger.warn(' The old environment variable is no longer used.'); + } + + if (process.env.ENABLE_AUTO_FEEDBACK_DELETION) { + DefaultLogger.warn( + '⚠️ Environment variable name has changed: ENABLE_AUTO_FEEDBACK_DELETION -> AUTO_FEEDBACK_DELETION_ENABLED', + ); + DefaultLogger.warn( + ` Current value in use: ${process.env.ENABLE_AUTO_FEEDBACK_DELETION}`, + ); + DefaultLogger.warn( + ' Please update the environment variable name to AUTO_FEEDBACK_DELETION_ENABLED.', + ); + DefaultLogger.warn(' The old environment variable is no longer used.'); + } + + if (process.env.SMTP_BASE_URL) { + DefaultLogger.warn( + '⚠️ Environment variable name has changed: SMTP_BASE_URL -> ADMIN_WEB_URL', + ); + DefaultLogger.warn( + ` Current SMTP_BASE_URL value: ${process.env.SMTP_BASE_URL}`, + ); + DefaultLogger.warn( + ' Please update to use ADMIN_WEB_URL instead of SMTP_BASE_URL.', + ); + DefaultLogger.warn(' The old environment variable is no longer used.'); + } } void bootstrap(); diff --git a/apps/api/src/scripts/build-swagger-docs.ts b/apps/api/src/scripts/build-swagger-docs.ts index a176b4bc9..ad9eb04e2 100644 --- a/apps/api/src/scripts/build-swagger-docs.ts +++ b/apps/api/src/scripts/build-swagger-docs.ts @@ -14,6 +14,7 @@ * under the License. */ import { writeFileSync } from 'fs'; +import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import type { NestFastifyApplication } from '@nestjs/platform-fastify'; import { FastifyAdapter } from '@nestjs/platform-fastify'; @@ -22,6 +23,7 @@ import { initializeTransactionalContext } from 'typeorm-transactional'; import { AppModule } from '../app.module'; import { APIModule } from '../domains/api/api.module'; +import type { ConfigServiceType } from '../types/config-service.type'; async function generateSwaggerDoc() { initializeTransactionalContext(); @@ -31,15 +33,24 @@ async function generateSwaggerDoc() { { bufferLogs: true }, ); - const documentConfig = new DocumentBuilder() + const configService = app.get(ConfigService); + const appConfig = configService.get('app', { infer: true }) ?? { + baseUrl: undefined, + }; + const baseUrl = appConfig.baseUrl; + + const documentConfigBuilder = new DocumentBuilder() .setTitle('User Feedback API Document') .setDescription( `You can use this API to integrate with your own service or system. This API is protected by a simple API key authentication, so please do not expose this API to the public. You can make an API key in the admin setting page. You should put the API key in the header with the key name 'x-api-key'. `, ) .setVersion('1.0.0') - .addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'apiKey') - .build(); + .addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'apiKey'); + if (baseUrl) { + documentConfigBuilder.addServer(baseUrl); + } + const documentConfig = documentConfigBuilder.build(); const document = SwaggerModule.createDocument(app, documentConfig, { include: [APIModule], }); diff --git a/apps/api/src/shared/code/code.service.spec.ts b/apps/api/src/shared/code/code.service.spec.ts index 6c68be033..4b38c8fe3 100644 --- a/apps/api/src/shared/code/code.service.spec.ts +++ b/apps/api/src/shared/code/code.service.spec.ts @@ -132,6 +132,45 @@ describe('CodeService', () => { }), ); }); + it('set code with custom duration', async () => { + const dto = new SetCodeEmailVerificationDto(); + dto.key = faker.string.sample(); + dto.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + dto.durationSec = 300; // 5 minutes + jest.spyOn(codeRepo, 'save'); + + const code = await codeService.setCode(dto); + + expect(code).toHaveLength(6); + expect(codeRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + code, + type: dto.type, + key: dto.key, + isVerified: false, + expiredAt: expect.any(Date), + }), + ); + }); + it('set code with default duration when durationSec is not provided', async () => { + const dto = new SetCodeEmailVerificationDto(); + dto.key = faker.string.sample(); + dto.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + jest.spyOn(codeRepo, 'save'); + + const code = await codeService.setCode(dto); + + expect(code).toHaveLength(6); + expect(codeRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + code, + type: dto.type, + key: dto.key, + isVerified: false, + expiredAt: expect.any(Date), + }), + ); + }); }); describe('verifyCode', () => { const key = faker.string.sample(); @@ -190,5 +229,195 @@ describe('CodeService', () => { expect(error).toEqual(new BadRequestException('code expired')); MockDate.reset(); }); + it('verifying code fails when already verified', async () => { + const { code, type } = codeFixture; + codeRepo.setIsVerified(true); + + const { error } = await codeService.verifyCode({ code, key, type }); + expect(error).toEqual(new BadRequestException('already verified')); + }); + it('verifying code increments try count on invalid code', async () => { + const { type } = codeFixture; + const invalidCode = faker.string.sample(6); + const initialTryCount = 0; // Start with 0 + const saveSpy = jest.spyOn(codeRepo, 'save'); + + // Mock findOne to return the entity with current tryCount + const mockEntity = { + ...codeFixture, + tryCount: initialTryCount, + key, + type, + isVerified: false, + expiredAt: new Date(Date.now() + 10 * 60 * 1000), // Future date + }; + jest.spyOn(codeRepo, 'findOne').mockResolvedValue(mockEntity as never); + + const { error } = await codeService.verifyCode({ + code: invalidCode, + key, + type, + }); + + expect(error).toEqual(new BadRequestException('invalid code')); + expect(saveSpy).toHaveBeenCalledWith( + expect.objectContaining({ + tryCount: initialTryCount + 1, + }), + ); + }); + it('updates existing code when key and type already exist', async () => { + const dto = new SetCodeEmailVerificationDto(); + dto.key = faker.string.sample(); + dto.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + + // Mock findOneBy to return an existing entity + const existingEntity = { + id: faker.number.int(), + type: dto.type, + key: dto.key, + code: faker.string.sample(6), + isVerified: false, + tryCount: 0, + expiredAt: new Date(), + data: null, + createdAt: new Date(), + updatedAt: new Date(), + } as CodeEntity; + + jest.spyOn(codeRepo, 'findOneBy').mockReturnValue(existingEntity); + const saveSpy = jest.spyOn(codeRepo, 'save'); + + const code = await codeService.setCode(dto); + + expect(code).toHaveLength(6); + expect(saveSpy).toHaveBeenCalledWith( + expect.objectContaining({ + code, + type: dto.type, + key: dto.key, + isVerified: false, + }), + ); + }); + }); + + describe('getDataByCodeAndType', () => { + it('returns data when code and type are valid', async () => { + const code = faker.string.sample(6); + const type = CodeTypeEnum.USER_INVITATION; + const expectedData = { + roleId: faker.number.int(), + userType: UserTypeEnum.GENERAL, + invitedBy: new UserDto(), + }; + codeRepo.setData(expectedData); + + const result = await codeService.getDataByCodeAndType(type, code); + + expect(result).toEqual(expectedData); + }); + + it('throws NotFoundException when code is not found', async () => { + const code = faker.string.sample(6); + const type = CodeTypeEnum.USER_INVITATION; + codeRepo.setNull(); + + await expect( + codeService.getDataByCodeAndType(type, code), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('checkVerified', () => { + it('returns true when code is verified', async () => { + const key = faker.string.sample(); + const type = CodeTypeEnum.EMAIL_VEIRIFICATION; + codeRepo.setIsVerified(true); + + const result = await codeService.checkVerified(type, key); + + expect(result).toBe(true); + }); + + it('returns false when code is not verified', async () => { + const key = faker.string.sample(); + const type = CodeTypeEnum.EMAIL_VEIRIFICATION; + codeRepo.setIsVerified(false); + + const result = await codeService.checkVerified(type, key); + + expect(result).toBe(false); + }); + + it('throws NotFoundException when code is not found', async () => { + const key = faker.string.sample(); + const type = CodeTypeEnum.EMAIL_VEIRIFICATION; + codeRepo.setNull(); + + await expect(codeService.checkVerified(type, key)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('createCode (private method)', () => { + it('generates 6-digit code', async () => { + const dto = new SetCodeEmailVerificationDto(); + dto.key = faker.string.sample(); + dto.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + + const code = await codeService.setCode(dto); + + expect(code).toHaveLength(6); + expect(code).toMatch(/^\d{6}$/); + }); + + it('generates different codes on multiple calls', async () => { + const dto1 = new SetCodeEmailVerificationDto(); + dto1.key = faker.string.sample(); + dto1.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + + const dto2 = new SetCodeEmailVerificationDto(); + dto2.key = faker.string.sample(); + dto2.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + + const code1 = await codeService.setCode(dto1); + const code2 = await codeService.setCode(dto2); + + expect(code1).not.toEqual(code2); + }); + }); + + describe('Edge cases and error scenarios', () => { + it('handles empty key gracefully', async () => { + const dto = new SetCodeEmailVerificationDto(); + dto.key = ''; + dto.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + + const code = await codeService.setCode(dto); + + expect(code).toHaveLength(6); + }); + + it('handles special characters in key', async () => { + const dto = new SetCodeEmailVerificationDto(); + dto.key = 'test@example.com'; + dto.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + + const code = await codeService.setCode(dto); + + expect(code).toHaveLength(6); + }); + + it('handles very long key', async () => { + const dto = new SetCodeEmailVerificationDto(); + dto.key = 'a'.repeat(1000); + dto.type = CodeTypeEnum.EMAIL_VEIRIFICATION; + + const code = await codeService.setCode(dto); + + expect(code).toHaveLength(6); + }); }); }); diff --git a/apps/api/src/shared/mailing/email-verification-mailing.service.spec.ts b/apps/api/src/shared/mailing/email-verification-mailing.service.spec.ts index ce6d04b79..899d50d2e 100644 --- a/apps/api/src/shared/mailing/email-verification-mailing.service.spec.ts +++ b/apps/api/src/shared/mailing/email-verification-mailing.service.spec.ts @@ -15,36 +15,191 @@ */ import { faker } from '@faker-js/faker'; import { MailerService } from '@nestjs-modules/mailer'; +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; -import { getMockProvider, TestConfig } from '@/test-utils/util-functions'; +import { TestConfig } from '@/test-utils/util-functions'; import { EmailVerificationMailingService } from './email-verification-mailing.service'; -describe('first', () => { +describe('EmailVerificationMailingService', () => { let emailVerificationMailingService: EmailVerificationMailingService; + let mockMailerService: jest.Mocked; + let mockConfigService: jest.Mocked; + beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestConfig], providers: [ EmailVerificationMailingService, - getMockProvider(MailerService, MockMailerService), + { + provide: MailerService, + useValue: { + sendMail: jest.fn(), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue('http://localhost:3000'), + }, + }, ], }).compile(); + emailVerificationMailingService = module.get( EmailVerificationMailingService, ); + mockMailerService = module.get(MailerService); + mockConfigService = module.get(ConfigService); }); - it('to be defined', () => { - expect(emailVerificationMailingService).toBeDefined(); + + afterEach(() => { + jest.clearAllMocks(); }); - it('send', async () => { - const code = faker.string.sample(); - const email = faker.internet.email(); - await emailVerificationMailingService.send({ code, email }); - expect(MockMailerService.sendMail).toHaveBeenCalledTimes(1); + + describe('constructor', () => { + it('should be defined', () => { + expect(emailVerificationMailingService).toBeDefined(); + }); + + it('should inject ConfigService', () => { + expect(mockConfigService).toBeDefined(); + }); }); -}); -const MockMailerService = { - sendMail: jest.fn(), -}; + describe('send', () => { + const mockCode = faker.string.alphanumeric(6); + const mockEmail = faker.internet.email(); + + it('should send email verification mail successfully', async () => { + await emailVerificationMailingService.send({ + code: mockCode, + email: mockEmail, + }); + + expect(mockMailerService.sendMail).toHaveBeenCalledTimes(1); + expect(mockMailerService.sendMail).toHaveBeenCalledWith({ + to: mockEmail, + subject: 'User feedback Email Verification', + context: { code: mockCode, baseUrl: 'http://localhost:3000' }, + template: 'verification', + }); + }); + + it('should call sendMail with correct parameters', async () => { + await emailVerificationMailingService.send({ + code: mockCode, + email: mockEmail, + }); + + const callArgs = mockMailerService.sendMail.mock.calls[0][0]; + expect(callArgs.to).toBe(mockEmail); + expect(callArgs.subject).toBe('User feedback Email Verification'); + expect(callArgs.context?.code).toBe(mockCode); + expect(callArgs.context?.baseUrl).toBe('http://localhost:3000'); + expect(callArgs.template).toBe('verification'); + }); + + it('should use empty string when baseUrl is not available', async () => { + // Configure ConfigService to return null + const module = await Test.createTestingModule({ + imports: [TestConfig], + providers: [ + EmailVerificationMailingService, + { + provide: MailerService, + useValue: { sendMail: jest.fn() }, + }, + { + provide: ConfigService, + useValue: { get: jest.fn().mockReturnValue(null) }, + }, + ], + }).compile(); + + const service = module.get(EmailVerificationMailingService); + const mailer = module.get(MailerService); + + await service.send({ code: mockCode, email: mockEmail }); + + const mockCalls = (mailer.sendMail as jest.Mock).mock + .calls as unknown[][]; + const callArgs = mockCalls[0]?.[0] as { context?: { baseUrl?: string } }; + expect(callArgs.context?.baseUrl).toBe(''); + }); + + it('should use empty string when smtp config is not available', async () => { + // Configure ConfigService to return undefined + const module = await Test.createTestingModule({ + imports: [TestConfig], + providers: [ + EmailVerificationMailingService, + { + provide: MailerService, + useValue: { sendMail: jest.fn() }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue(undefined), + }, + }, + ], + }).compile(); + + const service = module.get(EmailVerificationMailingService); + const mailer = module.get(MailerService); + + await service.send({ code: mockCode, email: mockEmail }); + + const mockCalls = (mailer.sendMail as jest.Mock).mock + .calls as unknown[][]; + const callArgs = mockCalls[0]?.[0] as { context?: { baseUrl?: string } }; + expect(callArgs.context?.baseUrl).toBe(''); + }); + + it('should throw error when mail sending fails', async () => { + const error = new Error('Mail sending failed'); + (mockMailerService.sendMail as jest.Mock).mockRejectedValue(error); + + await expect( + emailVerificationMailingService.send({ + code: mockCode, + email: mockEmail, + }), + ).rejects.toThrow('Mail sending failed'); + }); + + it('should handle various email formats correctly', async () => { + const testEmails = [ + faker.internet.email(), + faker.internet.email({ provider: 'gmail.com' }), + faker.internet.email({ provider: 'company.co.kr' }), + ]; + + for (const email of testEmails) { + await emailVerificationMailingService.send({ code: mockCode, email }); + expect(mockMailerService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ to: email }), + ); + } + }); + + it('should handle various verification code formats correctly', async () => { + const testCodes = [ + faker.string.alphanumeric(6), + faker.string.numeric(4), + faker.string.alpha(8), + ]; + + for (const code of testCodes) { + await emailVerificationMailingService.send({ code, email: mockEmail }); + expect(mockMailerService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ code }), + }), + ); + } + }); + }); +}); diff --git a/apps/api/src/shared/mailing/email-verification-mailing.service.ts b/apps/api/src/shared/mailing/email-verification-mailing.service.ts index eff5ee156..fb8c07a35 100644 --- a/apps/api/src/shared/mailing/email-verification-mailing.service.ts +++ b/apps/api/src/shared/mailing/email-verification-mailing.service.ts @@ -28,8 +28,7 @@ export class EmailVerificationMailingService { private readonly configService: ConfigService, ) { this.baseUrl = - (this.configService.get('smtp', { infer: true }) ?? { baseUrl: '' }) - .baseUrl ?? ''; + this.configService.get('app.adminWebUrl', { infer: true }) ?? ''; } async send({ code, email }: SendMailDto) { await this.mailerService.sendMail({ diff --git a/apps/api/src/shared/mailing/reset-password-mailing.service.spec.ts b/apps/api/src/shared/mailing/reset-password-mailing.service.spec.ts index cbce24bd0..c436430d5 100644 --- a/apps/api/src/shared/mailing/reset-password-mailing.service.spec.ts +++ b/apps/api/src/shared/mailing/reset-password-mailing.service.spec.ts @@ -15,36 +15,143 @@ */ import { faker } from '@faker-js/faker'; import { MailerService } from '@nestjs-modules/mailer'; +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import { getMockProvider, TestConfig } from '@/test-utils/util-functions'; +import type { ConfigServiceType } from '@/types/config-service.type'; import { ResetPasswordMailingService } from './reset-password-mailing.service'; describe('ResetPasswordMailingService', () => { let resetPasswordMailingService: ResetPasswordMailingService; + let mockConfigService: jest.Mocked>; + beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestConfig], providers: [ ResetPasswordMailingService, getMockProvider(MailerService, MockMailerService), + getMockProvider(ConfigService, MockConfigService), ], }).compile(); + resetPasswordMailingService = module.get(ResetPasswordMailingService); + mockConfigService = module.get(ConfigService); }); - it('to be defined', () => { - expect(resetPasswordMailingService).toBeDefined(); + describe('Basic functionality', () => { + it('should be defined', () => { + expect(resetPasswordMailingService).toBeDefined(); + }); }); - it('sends a mail', async () => { - const code = faker.string.sample(); - const email = faker.internet.email(); - await resetPasswordMailingService.send({ code, email }); + describe('send method', () => { + const mockBaseUrl = 'https://example.com'; + + beforeEach(() => { + MockMailerService.sendMail.mockClear(); + jest + .spyOn(mockConfigService, 'get') + .mockReturnValue({ baseUrl: mockBaseUrl }); + }); + + it('should send mail successfully', async () => { + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await resetPasswordMailingService.send({ code, email }); + + expect(MockMailerService.sendMail).toHaveBeenCalledTimes(1); + }); + + it('should send mail with correct parameters', async () => { + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await resetPasswordMailingService.send({ code, email }); + + expect(MockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User feedback Reset Password', + context: { + link: `/link/reset-password?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'resetPassword', + }); + }); + + it('should handle empty baseUrl correctly', async () => { + jest.spyOn(mockConfigService, 'get').mockReturnValue({ baseUrl: '' }); + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await resetPasswordMailingService.send({ code, email }); - expect(MockMailerService.sendMail).toHaveBeenCalledTimes(1); + expect(MockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User feedback Reset Password', + context: { + link: `/link/reset-password?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'resetPassword', + }); + }); + + it('should handle null configService response correctly', async () => { + jest.spyOn(mockConfigService, 'get').mockReturnValue(null); + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await resetPasswordMailingService.send({ code, email }); + + expect(MockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User feedback Reset Password', + context: { + link: `/link/reset-password?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'resetPassword', + }); + }); + + it('should handle special characters in code and email', async () => { + const code = 'test+code@123'; + const email = 'user+test@example.com'; + + await resetPasswordMailingService.send({ code, email }); + + expect(MockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User feedback Reset Password', + context: { + link: `/link/reset-password?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'resetPassword', + }); + }); + + it('should propagate errors from MailerService', async () => { + const error = new Error('Mailer service error'); + MockMailerService.sendMail.mockRejectedValue(error); + + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await expect( + resetPasswordMailingService.send({ code, email }), + ).rejects.toThrow(error); + }); }); }); const MockMailerService = { sendMail: jest.fn(), }; + +const MockConfigService = { + get: jest.fn().mockReturnValue({ baseUrl: 'https://example.com' }), +}; diff --git a/apps/api/src/shared/mailing/reset-password-mailing.service.ts b/apps/api/src/shared/mailing/reset-password-mailing.service.ts index ef4d78bf5..c45e6fe0c 100644 --- a/apps/api/src/shared/mailing/reset-password-mailing.service.ts +++ b/apps/api/src/shared/mailing/reset-password-mailing.service.ts @@ -28,8 +28,7 @@ export class ResetPasswordMailingService { private readonly configService: ConfigService, ) { this.baseUrl = - (this.configService.get('smtp', { infer: true }) ?? { baseUrl: '' }) - .baseUrl ?? ''; + this.configService.get('app.adminWebUrl', { infer: true }) ?? ''; } async send({ code, email }: SendMailDto) { await this.mailerService.sendMail({ diff --git a/apps/api/src/shared/mailing/user-invitation-mailing.service.spec.ts b/apps/api/src/shared/mailing/user-invitation-mailing.service.spec.ts index 505669d2b..1916359c1 100644 --- a/apps/api/src/shared/mailing/user-invitation-mailing.service.spec.ts +++ b/apps/api/src/shared/mailing/user-invitation-mailing.service.spec.ts @@ -15,36 +15,258 @@ */ import { faker } from '@faker-js/faker'; import { MailerService } from '@nestjs-modules/mailer'; +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import { getMockProvider, TestConfig } from '@/test-utils/util-functions'; +import type { ConfigServiceType } from '@/types/config-service.type'; import { UserInvitationMailingService } from './user-invitation-mailing.service'; -describe('first', () => { +describe('UserInvitationMailingService', () => { let userInvitationMailingService: UserInvitationMailingService; + let mockMailerService: jest.Mocked; + let mockConfigService: jest.Mocked>; + beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestConfig], providers: [ UserInvitationMailingService, getMockProvider(MailerService, MockMailerService), + getMockProvider(ConfigService, MockConfigService), ], }).compile(); + userInvitationMailingService = module.get(UserInvitationMailingService); + mockMailerService = module.get(MailerService); + mockConfigService = module.get(ConfigService); }); - it('to be defined', () => { - expect(userInvitationMailingService).toBeDefined(); + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('should be defined', () => { + expect(userInvitationMailingService).toBeDefined(); + }); + + it('should inject ConfigService', () => { + expect(mockConfigService).toBeDefined(); + }); }); - it('send', async () => { - const code = faker.string.sample(); - const email = faker.internet.email(); - await userInvitationMailingService.send({ code, email }); - expect(MockMailerService.sendMail).toHaveBeenCalledTimes(1); + describe('send', () => { + const mockBaseUrl = 'https://example.com'; + const mockCode = faker.string.alphanumeric(10); + const mockEmail = faker.internet.email(); + + beforeEach(() => { + MockMailerService.sendMail.mockClear(); + jest + .spyOn(mockConfigService, 'get') + .mockReturnValue({ baseUrl: mockBaseUrl }); + }); + + it('should send user invitation mail successfully', async () => { + await userInvitationMailingService.send({ + code: mockCode, + email: mockEmail, + }); + + expect(mockMailerService.sendMail).toHaveBeenCalledTimes(1); + expect(mockMailerService.sendMail).toHaveBeenCalledWith({ + to: mockEmail, + subject: 'User Feedback Invitation', + context: { + link: `/link/user-invitation?code=${mockCode}&email=${mockEmail}`, + baseUrl: '', + }, + template: 'invitation', + }); + }); + + it('should call sendMail with correct parameters', async () => { + await userInvitationMailingService.send({ + code: mockCode, + email: mockEmail, + }); + + const callArgs = mockMailerService.sendMail.mock.calls[0][0]; + expect(callArgs.to).toBe(mockEmail); + expect(callArgs.subject).toBe('User Feedback Invitation'); + expect(callArgs.context?.link).toBe( + `/link/user-invitation?code=${mockCode}&email=${mockEmail}`, + ); + expect(callArgs.context?.baseUrl).toBe(''); + expect(callArgs.template).toBe('invitation'); + }); + + it('should use empty string when baseUrl is not available', async () => { + jest.spyOn(mockConfigService, 'get').mockReturnValue({ baseUrl: '' }); + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await userInvitationMailingService.send({ code, email }); + + expect(mockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User Feedback Invitation', + context: { + link: `/link/user-invitation?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'invitation', + }); + }); + + it('should handle null configService response correctly', async () => { + jest.spyOn(mockConfigService, 'get').mockReturnValue(null); + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await userInvitationMailingService.send({ code, email }); + + expect(mockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User Feedback Invitation', + context: { + link: `/link/user-invitation?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'invitation', + }); + }); + + it('should handle undefined baseUrl in smtp config correctly', async () => { + jest + .spyOn(mockConfigService, 'get') + .mockReturnValue({ baseUrl: undefined }); + const code = faker.string.alphanumeric(10); + const email = faker.internet.email(); + + await userInvitationMailingService.send({ code, email }); + + expect(mockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User Feedback Invitation', + context: { + link: `/link/user-invitation?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'invitation', + }); + }); + + it('should handle special characters in code and email', async () => { + const code = 'test+code@123'; + const email = 'user+test@example.com'; + + await userInvitationMailingService.send({ code, email }); + + expect(mockMailerService.sendMail).toHaveBeenCalledWith({ + to: email, + subject: 'User Feedback Invitation', + context: { + link: `/link/user-invitation?code=${code}&email=${email}`, + baseUrl: '', + }, + template: 'invitation', + }); + }); + + it('should throw error when mail sending fails', async () => { + const error = new Error('Mail sending failed'); + mockMailerService.sendMail.mockRejectedValue(error); + + await expect( + userInvitationMailingService.send({ + code: mockCode, + email: mockEmail, + }), + ).rejects.toThrow('Mail sending failed'); + }); + + it('should handle various email formats correctly', async () => { + const testEmails = [ + faker.internet.email(), + faker.internet.email({ provider: 'gmail.com' }), + faker.internet.email({ provider: 'company.co.kr' }), + ]; + + for (const email of testEmails) { + await userInvitationMailingService.send({ code: mockCode, email }); + expect(mockMailerService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ to: email }), + ); + } + }); + + it('should handle various invitation code formats correctly', async () => { + const testCodes = [ + faker.string.alphanumeric(6), + faker.string.numeric(4), + faker.string.alpha(8), + faker.string.uuid(), + ]; + + for (const code of testCodes) { + await userInvitationMailingService.send({ code, email: mockEmail }); + expect(mockMailerService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + link: expect.stringContaining(`code=${code}`), + }), + }), + ); + } + }); + + it('should construct invitation link correctly with different baseUrls', async () => { + const testBaseUrls = [ + 'https://app.example.com', + 'http://localhost:3000', + 'https://staging.example.com', + '', + ]; + const mockCode = faker.string.alphanumeric(10); + const mockEmail = faker.internet.email(); + + for (const baseUrl of testBaseUrls) { + jest.spyOn(mockConfigService, 'get').mockReturnValue({ baseUrl }); + await userInvitationMailingService.send({ + code: mockCode, + email: mockEmail, + }); + + const expectedLink = `/link/user-invitation?code=${mockCode}&email=${mockEmail}`; + + expect(mockMailerService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + link: expectedLink, + baseUrl: '', + }), + }), + ); + } + }); + + it('should propagate errors from MailerService', async () => { + const error = new Error('Mailer service error'); + mockMailerService.sendMail.mockRejectedValue(error); + + await expect( + userInvitationMailingService.send({ code: mockCode, email: mockEmail }), + ).rejects.toThrow(error); + }); }); }); const MockMailerService = { sendMail: jest.fn(), }; + +const MockConfigService = { + get: jest.fn().mockReturnValue({ baseUrl: 'https://example.com' }), +}; diff --git a/apps/api/src/shared/mailing/user-invitation-mailing.service.ts b/apps/api/src/shared/mailing/user-invitation-mailing.service.ts index 7bf8a59b4..9b9d5916f 100644 --- a/apps/api/src/shared/mailing/user-invitation-mailing.service.ts +++ b/apps/api/src/shared/mailing/user-invitation-mailing.service.ts @@ -28,8 +28,7 @@ export class UserInvitationMailingService { private readonly configService: ConfigService, ) { this.baseUrl = - (this.configService.get('smtp', { infer: true }) ?? { baseUrl: '' }) - .baseUrl ?? ''; + this.configService.get('app.adminWebUrl', { infer: true }) ?? ''; } async send({ code, email }: SendMailDto) { diff --git a/apps/api/src/test-utils/stubs/code-repository.stub.ts b/apps/api/src/test-utils/stubs/code-repository.stub.ts index 28c561c42..8c5fedd2a 100644 --- a/apps/api/src/test-utils/stubs/code-repository.stub.ts +++ b/apps/api/src/test-utils/stubs/code-repository.stub.ts @@ -46,4 +46,15 @@ export class CodeRepositoryStub extends CommonRepositoryStub { entity.tryCount = tryCount; }); } + + getTryCount(): number { + return this.entities?.[0]?.tryCount ?? 0; + } + + setData(data: unknown) { + this.entities?.forEach((entity) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (entity as any).data = data; + }); + } } diff --git a/apps/api/src/test-utils/util-functions.ts b/apps/api/src/test-utils/util-functions.ts index a9c3a74be..e83c4412f 100644 --- a/apps/api/src/test-utils/util-functions.ts +++ b/apps/api/src/test-utils/util-functions.ts @@ -257,9 +257,11 @@ export const MockOpensearchRepository = { deleteIndex: jest.fn(), putMappings: jest.fn(), createData: jest.fn(), - getData: jest.fn(), + getData: jest.fn().mockResolvedValue({ items: [], total: 0 }), updateData: jest.fn(), getTotal: jest.fn(), + deleteBulkData: jest.fn(), + scroll: jest.fn().mockResolvedValue({ items: [], total: 0 }), }; export function removeUndefinedValues(obj: T): T { diff --git a/apps/api/src/utils/date-utils.spec.ts b/apps/api/src/utils/date-utils.spec.ts new file mode 100644 index 000000000..cf94c4d85 --- /dev/null +++ b/apps/api/src/utils/date-utils.spec.ts @@ -0,0 +1,131 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { DateTime } from 'luxon'; + +import { + calculateDaysBetweenDates, + getCurrentDay, + getCurrentMonth, + getCurrentYear, +} from './date-utils'; + +describe('date-utils', () => { + describe('calculateDaysBetweenDates', () => { + it('should calculate days between two dates correctly', () => { + const startDate = '2024-01-01'; + const endDate = '2024-01-10'; + + const result = calculateDaysBetweenDates(startDate, endDate); + + expect(result).toBe(9); + }); + + it('should return 0 when dates are the same', () => { + const date = '2024-01-01'; + + const result = calculateDaysBetweenDates(date, date); + + expect(result).toBe(0); + }); + + it('should return negative number when end date is before start date', () => { + const startDate = '2024-01-10'; + const endDate = '2024-01-01'; + + const result = calculateDaysBetweenDates(startDate, endDate); + + expect(result).toBe(-9); + }); + + it('should handle leap year correctly', () => { + const startDate = '2024-02-28'; + const endDate = '2024-03-01'; + + const result = calculateDaysBetweenDates(startDate, endDate); + + expect(result).toBe(2); + }); + + it('should handle different months correctly', () => { + const startDate = '2024-01-31'; + const endDate = '2024-02-01'; + + const result = calculateDaysBetweenDates(startDate, endDate); + + expect(result).toBe(1); + }); + + it('should handle different years correctly', () => { + const startDate = '2023-12-31'; + const endDate = '2024-01-01'; + + const result = calculateDaysBetweenDates(startDate, endDate); + + expect(result).toBe(1); + }); + }); + + describe('getCurrentYear', () => { + it('should return current year', () => { + const result = getCurrentYear(); + const expectedYear = DateTime.now().year; + + expect(result).toBe(expectedYear); + }); + + it('should return a valid year number', () => { + const result = getCurrentYear(); + + expect(typeof result).toBe('number'); + expect(result).toBeGreaterThan(2000); + expect(result).toBeLessThan(3000); + }); + }); + + describe('getCurrentMonth', () => { + it('should return current month', () => { + const result = getCurrentMonth(); + const expectedMonth = DateTime.now().month; + + expect(result).toBe(expectedMonth); + }); + + it('should return a valid month number (1-12)', () => { + const result = getCurrentMonth(); + + expect(typeof result).toBe('number'); + expect(result).toBeGreaterThanOrEqual(1); + expect(result).toBeLessThanOrEqual(12); + }); + }); + + describe('getCurrentDay', () => { + it('should return current day', () => { + const result = getCurrentDay(); + const expectedDay = DateTime.now().day; + + expect(result).toBe(expectedDay); + }); + + it('should return a valid day number (1-31)', () => { + const result = getCurrentDay(); + + expect(typeof result).toBe('number'); + expect(result).toBeGreaterThanOrEqual(1); + expect(result).toBeLessThanOrEqual(31); + }); + }); +}); diff --git a/apps/api/src/utils/escape-string-regexp.spec.ts b/apps/api/src/utils/escape-string-regexp.spec.ts new file mode 100644 index 000000000..32e239ab4 --- /dev/null +++ b/apps/api/src/utils/escape-string-regexp.spec.ts @@ -0,0 +1,139 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import escapeStringRegexp from './escape-string-regexp'; + +describe('escape-string-regexp', () => { + describe('escapeStringRegexp', () => { + it('should escape special regex characters', () => { + const input = 'test.string|with[special]characters'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\.string\\|with\\[special\\]characters'); + }); + + it('should escape parentheses', () => { + const input = 'test(string)with(parentheses)'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\(string\\)with\\(parentheses\\)'); + }); + + it('should escape curly braces', () => { + const input = 'test{string}with{braces}'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\{string\\}with\\{braces\\}'); + }); + + it('should escape square brackets', () => { + const input = 'test[string]with[brackets]'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\[string\\]with\\[brackets\\]'); + }); + + it('should escape backslashes', () => { + const input = 'test\\string\\with\\backslashes'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\\\string\\\\with\\\\backslashes'); + }); + + it('should escape pipe characters', () => { + const input = 'test|string|with|pipes'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\|string\\|with\\|pipes'); + }); + + it('should escape dollar signs', () => { + const input = 'test$string$with$dollars'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\$string\\$with\\$dollars'); + }); + + it('should escape plus signs', () => { + const input = 'test+string+with+pluses'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\+string\\+with\\+pluses'); + }); + + it('should escape asterisks', () => { + const input = 'test*string*with*asterisks'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\*string\\*with\\*asterisks'); + }); + + it('should escape question marks', () => { + const input = 'test?string?with?questions'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\?string\\?with\\?questions'); + }); + + it('should escape carets', () => { + const input = 'test^string^with^carets'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\^string\\^with\\^carets'); + }); + + it('should escape hyphens with \\x2d', () => { + const input = 'test-string-with-hyphens'; + const result = escapeStringRegexp(input); + + expect(result).toBe('test\\x2dstring\\x2dwith\\x2dhyphens'); + }); + + it('should handle empty string', () => { + const input = ''; + const result = escapeStringRegexp(input); + + expect(result).toBe(''); + }); + + it('should handle string without special characters', () => { + const input = 'normalstring'; + const result = escapeStringRegexp(input); + + expect(result).toBe('normalstring'); + }); + + it('should handle string with only special characters', () => { + const input = '|\\{}()[\\]^$+*?.-'; + const result = escapeStringRegexp(input); + + expect(result).toBe( + '\\|\\\\\\{\\}\\(\\)\\[\\\\\\]\\^\\$\\+\\*\\?\\.\\x2d', + ); + }); + + it('should throw TypeError for non-string input', () => { + expect(() => escapeStringRegexp(null as any)).toThrow(TypeError); + expect(() => escapeStringRegexp(undefined as any)).toThrow(TypeError); + expect(() => escapeStringRegexp(123 as any)).toThrow(TypeError); + expect(() => escapeStringRegexp({} as any)).toThrow(TypeError); + expect(() => escapeStringRegexp([] as any)).toThrow(TypeError); + }); + + it('should throw TypeError with correct message', () => { + expect(() => escapeStringRegexp(123 as any)).toThrow('Expected a string'); + }); + }); +}); diff --git a/apps/api/src/utils/validate-unique.spec.ts b/apps/api/src/utils/validate-unique.spec.ts new file mode 100644 index 000000000..55235923e --- /dev/null +++ b/apps/api/src/utils/validate-unique.spec.ts @@ -0,0 +1,189 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { validateUnique } from './validate-unique'; + +describe('validate-unique', () => { + describe('validateUnique', () => { + interface TestObject { + id: number; + name: string; + value: string; + } + + it('should return true for unique values', () => { + const objList: TestObject[] = [ + { id: 1, name: 'test1', value: 'value1' }, + { id: 2, name: 'test2', value: 'value2' }, + { id: 3, name: 'test3', value: 'value3' }, + ]; + + const result = validateUnique(objList, 'id'); + expect(result).toBe(true); + }); + + it('should return false for duplicate values', () => { + const objList: TestObject[] = [ + { id: 1, name: 'test1', value: 'value1' }, + { id: 2, name: 'test2', value: 'value2' }, + { id: 1, name: 'test3', value: 'value3' }, + ]; + + const result = validateUnique(objList, 'id'); + expect(result).toBe(false); + }); + + it('should work with string properties', () => { + const objList: TestObject[] = [ + { id: 1, name: 'test1', value: 'value1' }, + { id: 2, name: 'test2', value: 'value2' }, + { id: 3, name: 'test3', value: 'value3' }, + ]; + + const result = validateUnique(objList, 'name'); + expect(result).toBe(true); + }); + + it('should return false for duplicate string values', () => { + const objList: TestObject[] = [ + { id: 1, name: 'test1', value: 'value1' }, + { id: 2, name: 'test2', value: 'value2' }, + { id: 3, name: 'test1', value: 'value3' }, + ]; + + const result = validateUnique(objList, 'name'); + expect(result).toBe(false); + }); + + it('should handle empty array', () => { + const objList: TestObject[] = []; + + const result = validateUnique(objList, 'id'); + expect(result).toBe(true); + }); + + it('should handle undefined array', () => { + const objList: TestObject[] | undefined = undefined; + + const result = validateUnique(objList, 'id'); + expect(result).toBe(true); + }); + + it('should handle single item array', () => { + const objList: TestObject[] = [{ id: 1, name: 'test1', value: 'value1' }]; + + const result = validateUnique(objList, 'id'); + expect(result).toBe(true); + }); + + it('should work with different property types', () => { + interface MixedObject { + id: number; + name: string; + isActive: boolean; + tags: string[]; + } + + const objList: MixedObject[] = [ + { id: 1, name: 'test1', isActive: true, tags: ['tag1'] }, + { id: 2, name: 'test2', isActive: false, tags: ['tag2'] }, + { id: 3, name: 'test3', isActive: true, tags: ['tag3'] }, + ]; + + const result = validateUnique(objList, 'isActive'); + expect(result).toBe(false); // true and false are different, but we have two true values + }); + + it('should work with boolean properties', () => { + interface BooleanObject { + id: number; + isActive: boolean; + } + + const objList: BooleanObject[] = [ + { id: 1, isActive: true }, + { id: 2, isActive: false }, + { id: 3, isActive: true }, + ]; + + const result = validateUnique(objList, 'isActive'); + expect(result).toBe(false); + }); + + it('should work with unique boolean values', () => { + interface BooleanObject { + id: number; + isActive: boolean; + } + + const objList: BooleanObject[] = [ + { id: 1, isActive: true }, + { id: 2, isActive: false }, + ]; + + const result = validateUnique(objList, 'isActive'); + expect(result).toBe(true); + }); + + it('should handle null and undefined values', () => { + interface NullableObject { + id: number; + value: string | null | undefined; + } + + const objList: NullableObject[] = [ + { id: 1, value: 'test1' }, + { id: 2, value: null }, + { id: 3, value: undefined }, + { id: 4, value: 'test2' }, + ]; + + const result = validateUnique(objList, 'value'); + expect(result).toBe(true); // null, undefined, 'test1', 'test2' are all different + }); + + it('should handle duplicate null values', () => { + interface NullableObject { + id: number; + value: string | null; + } + + const objList: NullableObject[] = [ + { id: 1, value: 'test1' }, + { id: 2, value: null }, + { id: 3, value: null }, + ]; + + const result = validateUnique(objList, 'value'); + expect(result).toBe(false); + }); + + it('should handle duplicate undefined values', () => { + interface UndefinedObject { + id: number; + value: string | undefined; + } + + const objList: UndefinedObject[] = [ + { id: 1, value: 'test1' }, + { id: 2, value: undefined }, + { id: 3, value: undefined }, + ]; + + const result = validateUnique(objList, 'value'); + expect(result).toBe(false); + }); + }); +}); diff --git a/apps/cli/.gitignore b/apps/cli/.gitignore new file mode 100644 index 000000000..ab8b69cbc --- /dev/null +++ b/apps/cli/.gitignore @@ -0,0 +1 @@ +config.toml \ No newline at end of file diff --git a/apps/cli/bin/auf-cli.ts b/apps/cli/bin/auf-cli.ts deleted file mode 100755 index 8e4c7305c..000000000 --- a/apps/cli/bin/auf-cli.ts +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env node -/** - * Copyright 2025 LY Corporation - * - * LY Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { execSync } from 'child_process'; -import * as fs from 'fs'; -import os from 'os'; -import * as path from 'path'; -import { Command } from 'commander'; -import { load } from 'js-toml'; - -const program = new Command(); - -program.description( - 'ABC User Feedback CLI that helps to run web frontend and server easily.', -); - -function getArchitectureType() { - const arch = os.arch(); - - switch (arch) { - case 'arm': - case 'arm64': - return 'arm'; - case 'ia32': - case 'x32': - case 'x64': - return 'amd'; - default: - return 'arm'; - } -} - -program - .command('init') - .description( - 'Start the appropriate Docker Compose file based on architecture to setup the ABC User Feedback infrastructure.', - ) - .action(() => { - const architecture = getArchitectureType(); - console.log(`Your system architecture is detected as: ${architecture}`); - - const composeFile = - architecture === 'amd' ? - 'docker-compose.infra-amd64.yml' - : 'docker-compose.infra-arm64.yml'; - - const composeFilePath = path.join(__dirname + '/../', composeFile); - console.log( - `Terminates existing Docker Compose with auf-cli project name...`, - ); - execSync(`docker compose -p auf-cli down`); - - console.log(`Running Docker Compose with ${composeFilePath.toString()}...`); - execSync( - `docker compose -p auf-cli -f ${composeFilePath.toString()} up -d`, - { - stdio: 'inherit', - }, - ); - - const sourceConfigPath = path.join(__dirname + '/../config.toml'); - const destinationConfigPath = path.join(process.cwd(), 'config.toml'); - fs.copyFileSync(sourceConfigPath, destinationConfigPath); - console.log( - 'config.toml has been created. Please fill in the required environment variables.', - ); - }); - -program - .command('start') - .description( - 'Pull ABC User Feedback Docker image and run container with environment variables', - ) - .action(() => { - if (fs.existsSync(path.join(process.cwd(), 'config.toml')) === false) { - console.error( - 'config.toml file is missing. Please run "npx auf-cli init" first.', - ); - return; - } - - const destinationConfigPath = path.join(process.cwd(), 'config.toml'); - - const templatePath = path.join( - __dirname + '/../docker-compose.template.yml', - ); - - interface TomlConfig { - web: Record; - api: Record; - } - - const tomlContent = fs.readFileSync(destinationConfigPath, 'utf-8'); - const tomlConfig = load(tomlContent) as TomlConfig; - - const webEnvVars = ['NEXT_PUBLIC_API_BASE_URL']; - - const apiEnvVars = [ - 'JWT_SECRET', - 'MYSQL_PRIMARY_URL', - 'BASE_URL', - 'SMTP_HOST', - 'SMTP_PORT', - 'SMTP_SENDER', - 'SMTP_BASE_URL', - 'AUTO_MIGRATION', - 'MASTER_API_KEY', - 'NODE_OPTIONS', - ]; - - const missingWebEnvVars = webEnvVars.filter( - (varName) => !tomlConfig.web[varName], - ); - const missingApiEnvVars = apiEnvVars.filter( - (varName) => !tomlConfig.api[varName], - ); - - if (missingWebEnvVars.length > 0) { - console.error( - `Missing required environment variables for web service: ${missingWebEnvVars.join(', ')}`, - ); - process.exit(1); - } - - if (missingApiEnvVars.length > 0) { - console.error( - `Missing required environment variables for api service: ${missingApiEnvVars.join(', ')}`, - ); - process.exit(1); - } - - let dockerComposeTemplate = fs.readFileSync(templatePath, 'utf-8'); - - webEnvVars.forEach((varName) => { - const regex = new RegExp(`\\$\\{${varName}\\}`, 'g'); - dockerComposeTemplate = dockerComposeTemplate.replace( - regex, - tomlConfig.web[varName], - ); - }); - apiEnvVars.forEach((varName) => { - const regex = new RegExp(`\\$\\{${varName}\\}`, 'g'); - dockerComposeTemplate = dockerComposeTemplate.replace( - regex, - tomlConfig.api[varName], - ); - }); - - const dockerComposePath = path.resolve( - process.cwd(), - 'docker-compose.generated.yml', - ); - fs.writeFileSync(dockerComposePath, dockerComposeTemplate); - - console.log('docker-compose.generated.yml has been created'); - - const apiDockerImage = 'line/abc-user-feedback-api'; - const webDockerImage = 'line/abc-user-feedback-web'; - - console.log(`Pulling Docker image ${apiDockerImage}, ${webDockerImage}...`); - execSync(`docker pull ${apiDockerImage}`); - execSync(`docker pull ${webDockerImage}`); - - const dockerComposeCommand = `docker compose -p auf-cli -f ${dockerComposePath} up -d`; - console.log(`Running Docker Compose with command: ${dockerComposeCommand}`); - execSync(dockerComposeCommand); - - console.log( - '\x1b[32m', - '\nStarted ABC User Feedback services.\n', - '\x1b[0m', - ); - const serviceInfos = { - 'API URL': 'http://localhost:4000', - 'WEB URL': 'http://localhost:3000', - 'DB URL': 'http://localhost:13306', - 'OPENSEARCH URL': 'http://localhost:9200', - 'OPENSEARCH ADMIN URL': 'http://localhost:5601', - JWT_SECRET: tomlConfig.api.JWT_SECRET, - }; - - for (const [key, value] of Object.entries(serviceInfos)) { - console.log(`${key.padStart(20)}: ${value}`); - } - }); - -program - .command('stop') - .description('Stop the running Docker containers for app and web services') - .action(() => { - const dockerComposeCommand = `docker compose -p auf-cli down`; - console.log( - `Stopping Docker Compose with command: ${dockerComposeCommand}`, - ); - execSync(dockerComposeCommand); - }); - -program - .command('clean') - .description('Delete existing mounted docker volumes') - .action(() => { - console.log('Deletes existing mounted docker volumes...'); - execSync(`rm -rf ${path.join(__dirname, '../volumes')}`); - }); - -program.parse(process.argv); diff --git a/apps/cli/bin/compose.ts b/apps/cli/bin/compose.ts new file mode 100644 index 000000000..f29577af8 --- /dev/null +++ b/apps/cli/bin/compose.ts @@ -0,0 +1,204 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import YAML from 'yaml'; + +import type { AppConfig } from './config'; + +export function generateComposeContent(cfg: AppConfig) { + const doc = { + name: 'abc-user-feedback', + services: { + web: { + image: 'line/abc-user-feedback-web:latest', + ports: [`${cfg.web.port}:3000`], + depends_on: { api: { condition: 'service_healthy' } }, + restart: 'unless-stopped', + environment: [ + `NEXT_PUBLIC_API_BASE_URL=http://localhost:${cfg.api.port}`, + ], + }, + api: { + image: 'line/abc-user-feedback-api:latest', + environment: [ + `JWT_SECRET=${cfg.api.jwt_secret}`, + 'MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@mysql:3306/userfeedback', + `SMTP_HOST=${cfg.api.smtp.host}`, + `SMTP_PORT=${cfg.api.smtp.port}`, + `SMTP_SENDER=${cfg.api.smtp.sender}`, + ], + ports: [`${cfg.api.port}:4000`], + depends_on: { mysql: { condition: 'service_healthy' } }, + restart: 'unless-stopped', + healthcheck: { + test: [ + 'CMD-SHELL', + "node -e \"require('http').get('http://localhost:4000/api/health', res => process.exit(res.statusCode === 200 ? 0 : 1))\"", + ], + interval: '10s', + timeout: '5s', + retries: '5', + }, + }, + smtp4dev: { + image: 'rnwood/smtp4dev:v3', + ports: ['5080:80', '25:25', '143:143'], + volumes: ['smtp4dev:/smtp4dev'], + restart: 'unless-stopped', + }, + mysql: { + image: 'mysql:8.0', + command: [ + '--default-authentication-plugin=mysql_native_password', + '--collation-server=utf8mb4_bin', + ], + environment: { + MYSQL_ROOT_PASSWORD: 'userfeedback', + MYSQL_DATABASE: 'userfeedback', + MYSQL_USER: 'userfeedback', + MYSQL_PASSWORD: 'userfeedback', + TZ: 'UTC', + }, + ports: [`${cfg.mysql?.port}:3306`], + volumes: ['mysql:/var/lib/mysql'], + restart: 'unless-stopped', + healthcheck: { + test: [ + 'CMD', + 'mysqladmin', + 'ping', + '-h', + 'localhost', + '-uuserfeedback', + '-puserfeedback', + ], + interval: '10s', + timeout: '5s', + retries: '5', + }, + }, + }, + volumes: { mysql: {}, smtp4dev: {} } as Record, + }; + + const apiEnvVariables = { + MASTER_API_KEY: cfg.api.master_api_key, + ACCESS_TOKEN_EXPIRED_TIME: cfg.api.access_token_expired_time, + REFRESH_TOKEN_EXPIRED_TIME: cfg.api.refresh_token_expired_time, + SMTP_USERNAME: cfg.api.smtp.username, + SMTP_PASSWORD: cfg.api.smtp.password, + SMTP_TLS: cfg.api.smtp.tls, + SMTP_CIPHER_SPEC: cfg.api.smtp.cipher_spec, + SMTP_OPPORTUNISTIC_TLS: cfg.api.smtp.opportunistic_tls, + AUTO_FEEDBACK_DELETION_ENABLED: cfg.api.auto_feedback_deletion?.enabled, + AUTO_FEEDBACK_DELETION_PERIOD_DAYS: + cfg.api.auto_feedback_deletion?.period_days, + OPENSEARCH_USE: cfg.api.opensearch?.enabled, + }; + + for (const [key, value] of Object.entries(apiEnvVariables)) { + if (value !== undefined) { + doc.services.api.environment.push(`${key}=${value}`); + } + if (key === 'OPENSEARCH_USE' && value === true) { + doc.services.api.environment.push( + `OPENSEARCH_NODE=http://opensearch-node:9200`, + ); + } + } + if (cfg.mysql) { + doc.services.mysql = { + image: 'mysql:8.0', + command: [ + '--default-authentication-plugin=mysql_native_password', + '--collation-server=utf8mb4_bin', + ], + environment: { + MYSQL_ROOT_PASSWORD: 'userfeedback', + MYSQL_DATABASE: 'userfeedback', + MYSQL_USER: 'userfeedback', + MYSQL_PASSWORD: 'userfeedback', + TZ: 'UTC', + }, + ports: [`${cfg.mysql.port}:3306`], + volumes: ['mysql:/var/lib/mysql'], + restart: 'unless-stopped', + healthcheck: { + test: [ + 'CMD', + 'mysqladmin', + 'ping', + '-h', + 'localhost', + '-uuserfeedback', + '-puserfeedback', + ], + interval: '10s', + timeout: '5s', + retries: '5', + }, + }; + } + + if (cfg.api.opensearch) { + doc.services['opensearch-node'] = { + image: 'opensearchproject/opensearch:2.16.0', + restart: 'unless-stopped', + environment: [ + 'cluster.name=opensearch-cluster', + 'node.name=opensearch-node', + 'discovery.type=single-node', + 'bootstrap.memory_lock=true', + 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m', + 'plugins.security.disabled=true', + 'OPENSEARCH_INITIAL_ADMIN_PASSWORD=UserFeedback123!@#', + ], + ulimits: { + memlock: { soft: -1, hard: -1 }, + nofile: { soft: 65536, hard: 65536 }, + }, + volumes: ['opensearch:/usr/share/opensearch/data'], + ports: ['9200:9200', '9600:9600'], + healthcheck: { + test: ['CMD', 'curl', '-f', 'http://localhost:9200/_cluster/health'], + interval: '10s', + timeout: '5s', + retries: '5', + }, + }; + doc.services.api.depends_on['opensearch-node'] = { + condition: 'service_healthy', + }; + doc.volumes.opensearch = {}; + + doc.services['opensearch-dashboards'] = { + image: 'opensearchproject/opensearch-dashboards:2.16.0', + restart: 'unless-stopped', + ports: ['5601:5601'], + environment: [ + 'OPENSEARCH_HOSTS=["http://opensearch-node:9200"]', + 'DISABLE_SECURITY_DASHBOARDS_PLUGIN=true', + ], + depends_on: { + 'opensearch-node': { + condition: 'service_healthy', + }, + }, + }; + } + + const yml = YAML.stringify(doc); + return yml; +} diff --git a/apps/cli/bin/config.ts b/apps/cli/bin/config.ts new file mode 100644 index 000000000..da822fd89 --- /dev/null +++ b/apps/cli/bin/config.ts @@ -0,0 +1,73 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import path from 'path'; +import * as TOML from 'toml'; +import { z } from 'zod'; + +import { exists, readFile } from './fsutil'; + +const ConfigSchema = z.object({ + web: z.object({ + port: z.number().default(3000), + api_base_url: z.string().default('http://localhost:4000'), + }), + api: z.object({ + port: z.number().default(4000), + jwt_secret: z.string().min(32).default('jwtsecretjwtsecretjwtsecret'), + master_api_key: z.string().optional(), + access_token_expired_time: z.string().optional(), + refresh_token_expired_time: z.string().optional(), + auto_feedback_deletion: z + .object({ + enabled: z.boolean().default(false), + period_days: z.number().optional(), + }) + .optional(), + smtp: z + .object({ + host: z.string(), + port: z.number(), + sender: z.string(), + username: z.string().optional(), + password: z.string().optional(), + tls: z.string().optional(), + cipher_spec: z.string().optional(), + opportunistic_tls: z.string().optional(), + }) + .optional() + .default({ host: 'smtp4dev', port: 25, sender: 'user@feedback.com' }), + opensearch: z.object({ enabled: z.boolean().default(false) }).optional(), + }), + mysql: z.object({ port: z.number().default(13306) }).optional(), +}); + +export type AppConfig = z.infer; + +export function loadConfig(cwd = process.cwd()): AppConfig { + const mainPath = path.join(cwd, 'config.toml'); + if (!exists(mainPath)) + throw new Error( + "config.toml 이 없습니다. 먼저 'mystack init'을 실행하세요.", + ); + const main = TOML.parse(readFile(mainPath)) as unknown; + const parsed = ConfigSchema.safeParse(main); + if (!parsed.success) + throw new Error( + 'config.toml 검증 실패:\n' + + JSON.stringify(parsed.error.format(), null, 2), + ); + return parsed.data; +} diff --git a/apps/cli/bin/fsutil.ts b/apps/cli/bin/fsutil.ts new file mode 100644 index 000000000..96141c782 --- /dev/null +++ b/apps/cli/bin/fsutil.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import fs from 'fs'; +import path from 'path'; + +export function ensureDir(dir: string) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} +export function writeFile(p: string, content: string) { + ensureDir(path.dirname(p)); + fs.writeFileSync(p, content, 'utf8'); +} +export function exists(p: string) { + return fs.existsSync(p); +} +export function readFile(p: string) { + return fs.readFileSync(p, 'utf8'); +} diff --git a/apps/cli/bin/index.ts b/apps/cli/bin/index.ts new file mode 100755 index 000000000..124b5f24c --- /dev/null +++ b/apps/cli/bin/index.ts @@ -0,0 +1,145 @@ +#!/usr/bin/env node +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { Command } from 'commander'; + +import packageJson from '../package.json'; +import { generateComposeContent } from './compose'; +import { loadConfig } from './config'; +import { exists, writeFile } from './fsutil'; +import { run, runWithStdin } from './shell'; + +const program = new Command(); +program + .name('auf-cli') + .description('Tiny stack CLI (config.toml only)') + .version(packageJson.version); + +program + .command('init') + .description('Generate config.toml template — use without .env') + .option('--force', 'Overwrite existing file') + .action((opts: { force?: boolean }) => { + if (exists('config.toml') && !opts.force) + throw new Error('config.toml already exists. Use --force to overwrite.'); + writeFile('config.toml', defaultConfigToml()); + console.log('✅ Created: config.toml'); + }); + +program + .command('start') + .description('Start services with docker compose up -d based on config.toml') + .action(async () => { + const cfg = loadConfig(); + + const composeContent = generateComposeContent(cfg); + await runWithStdin( + 'docker', + ['compose', '-f', '-', 'up', '-d', '--remove-orphans'], + composeContent, + ); + + console.log('🚀 Services started successfully!'); + console.log('🔗 Available URLs:'); + console.log(` 📱 Web: http://localhost:${cfg.web.port}`); + console.log(` 🔧 API: http://localhost:${cfg.api.port}`); + if (cfg.mysql) { + console.log( + ` 🗄️ MySQL: mysql://userfeedback:userfeedback@localhost:${cfg.mysql.port}`, + ); + } + if (cfg.api.opensearch?.enabled) { + console.log(` 🔍 OpenSearch: http://localhost:9200`); + } + if (cfg.api.smtp.host === 'smtp4dev') { + console.log(` 📧 SMTP Mail Web: http://localhost:5080`); + } + }); + +program + .command('stop') + .description('Stop services with docker compose down') + .action(async () => { + const cfg = loadConfig(); + + const composeContent = generateComposeContent(cfg); + await runWithStdin( + 'docker', + ['compose', '-f', '-', 'down'], + composeContent, + ); + console.log('🛑 Services stopped successfully'); + }); + +program + .command('clean') + .description('Clean up containers/networks/volumes') + .option('--images', 'Also prune images') + .action(async (opts: { images?: boolean }) => { + const cfg = loadConfig(); + + const composeContent = generateComposeContent(cfg); + await runWithStdin( + 'docker', + ['compose', '-f', '-', 'down', '--volumes', '--remove-orphans'], + composeContent, + ); + + if (opts.images) await run('docker', ['image', 'prune', '-f']); + console.log('🧹 Cleanup completed successfully'); + }); + +program.parseAsync().catch((e: Error) => { + console.error('❌ Error:', e.message); + process.exit(1); +}); + +function defaultConfigToml() { + return ` +[web] +port = 3000 +# api_base_url = "http://localhost:4000" + +[api] +port = 4000 +jwt_secret = "jwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecret" + +# master_api_key = "MASTER_KEY" +# access_token_expired_time = "10m" +# refresh_token_expired_time = "1h" + +# [api.auto_feedback_deletion] +# enabled = true +# period_days = 365 + +# [api.smtp] +# host = "smtp4dev" # SMTP_HOST +# port = 25 # SMTP_PORT +# sender = "user@feedback.com" +# username= +# password= +# tls= +# ciper_spec= +# opportunitic_tls= + + +# [api.opensearch] +# enabled = true + +[mysql] +port = 13306 +`; +} diff --git a/apps/cli/bin/shell.ts b/apps/cli/bin/shell.ts new file mode 100644 index 000000000..78f488fab --- /dev/null +++ b/apps/cli/bin/shell.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { spawn } from 'child_process'; + +export function run(cmd: string, args: string[] = [], cwd?: string) { + return new Promise((resolve, reject) => { + const p = spawn(cmd, args, { + stdio: 'inherit', + cwd, + shell: process.platform === 'win32', + }); + p.on('close', (code) => + code === 0 ? resolve() : ( + reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`)) + ), + ); + }); +} + +export function runWithStdin( + cmd: string, + args: string[] = [], + input: string, + cwd?: string, +) { + return new Promise((resolve, reject) => { + const p = spawn(cmd, args, { + stdio: ['pipe', 'inherit', 'inherit'], + cwd, + shell: process.platform === 'win32', + }); + + p.stdin.write(input); + p.stdin.end(); + + p.on('close', (code) => + code === 0 ? resolve() : ( + reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`)) + ), + ); + }); +} diff --git a/apps/cli/config.toml b/apps/cli/config.toml deleted file mode 100644 index b1795c527..000000000 --- a/apps/cli/config.toml +++ /dev/null @@ -1,19 +0,0 @@ -[api] -MYSQL_PRIMARY_URL="mysql://root:userfeedback@host.docker.internal:13306/userfeedback" -# MYSQL_SECONDARY_URLS= ["mysql://root:userfeedback@host.docker.internal:13306/userfeedback"] -OPENSEARCH_USE=true -OPENSEARCH_NODE="http://localhost:9200" -OPENSEARCH_USERNAME="UserFeedback123!@#" -SMTP_HOST="localhost" -SMTP_PORT=25 -SMTP_SENDER="noreply@linecorp.com" -SMTP_BASE_URL="http://localhost:3000" -AUTO_MIGRATION=true -NODE_OPTIONS="--max_old_space_size=3072" -BASE_URL="http://localhost:3000" -JWT_SECRET="secret" -OPENSEARCH_PASSWORD="UserFeedback123!@#" -MASTER_API_KEY="MASTER_API_KEY" - -[web] -NEXT_PUBLIC_API_BASE_URL="http://localhost:4000" diff --git a/apps/cli/docker-compose.infra-amd64.yml b/apps/cli/docker-compose.infra-amd64.yml deleted file mode 100644 index 927373be6..000000000 --- a/apps/cli/docker-compose.infra-amd64.yml +++ /dev/null @@ -1,106 +0,0 @@ -services: - mysql: - hostname: mysql - image: mysql:8.0.39 - platform: linux/amd64 - restart: always - command: - [ - '--default-authentication-plugin=mysql_native_password', - '--collation-server=utf8mb4_bin', - ] - environment: - MYSQL_ROOT_PASSWORD: userfeedback - MYSQL_DATABASE: userfeedback - MYSQL_USER: userfeedback - MYSQL_PASSWORD: userfeedback - TZ: UTC - ports: - - 13306:3306 - volumes: - - ./volumes/mysql:/var/lib/mysql - networks: - - app_network - - # optional for e2e test - mysql-for-e2e: - hostname: mysql - image: mysql:8.0.39 - platform: linux/amd64 - restart: always - command: - [ - '--default-authentication-plugin=mysql_native_password', - '--collation-server=utf8mb4_bin', - ] - environment: - MYSQL_ROOT_PASSWORD: userfeedback - MYSQL_DATABASE: e2e - MYSQL_USER: userfeedback - MYSQL_PASSWORD: userfeedback - TZ: UTC - ports: - - 13307:3306 - volumes: - - ./volumes/mysql-for-e2e:/var/lib/mysql-for-e2e - networks: - - app_network - - # optional for email verification on creating user - smtp4dev: - image: rnwood/smtp4dev:v3 - restart: always - ports: - - 5080:80 - - 25:25 - - 143:143 - volumes: - - ./volumes/smtp4dev:/smtp4dev - networks: - - app_network - - # optional for better performance on searching feedbacks - opensearch-node: - image: opensearchproject/opensearch:2.15.0 - restart: always - container_name: opensearch-node - environment: - - cluster.name=opensearch-cluster - - node.name=opensearch-node - - discovery.type=single-node - - bootstrap.memory_lock=true - - 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m' - - plugins.security.disabled=true - - OPENSEARCH_INITIAL_ADMIN_PASSWORD=UserFeedback123!@# - ulimits: - memlock: - soft: -1 - hard: -1 - nofile: - soft: 65536 - hard: 65536 - volumes: - - ./volumes/opensearch:/usr/share/opensearch/data - ports: - - 9200:9200 - - 9600:9600 - networks: - - app_network - - # optional for opensearch - opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:2.15.0 - restart: always - container_name: opensearch-dashboards - ports: - - 5601:5601 - environment: - - 'OPENSEARCH_HOSTS=["http://opensearch-node:9200"]' - - 'DISABLE_SECURITY_DASHBOARDS_PLUGIN=true' - depends_on: - - opensearch-node - networks: - - app_network - -networks: - app_network: diff --git a/apps/cli/docker-compose.infra-arm64.yml b/apps/cli/docker-compose.infra-arm64.yml deleted file mode 100644 index 4fbfe2d13..000000000 --- a/apps/cli/docker-compose.infra-arm64.yml +++ /dev/null @@ -1,106 +0,0 @@ -services: - mysql: - hostname: mysql - image: mysql:8.0.39 - platform: linux/arm64/v8 - restart: always - command: - [ - '--default-authentication-plugin=mysql_native_password', - '--collation-server=utf8mb4_bin', - ] - environment: - MYSQL_ROOT_PASSWORD: userfeedback - MYSQL_DATABASE: userfeedback - MYSQL_USER: userfeedback - MYSQL_PASSWORD: userfeedback - TZ: UTC - ports: - - 13306:3306 - volumes: - - ./volumes/mysql:/var/lib/mysql - networks: - - app_network - - # optional for e2e test - mysql-for-e2e: - hostname: mysql - image: mysql:8.0.39 - platform: linux/arm64/v8 - restart: always - command: - [ - '--default-authentication-plugin=mysql_native_password', - '--collation-server=utf8mb4_bin', - ] - environment: - MYSQL_ROOT_PASSWORD: userfeedback - MYSQL_DATABASE: e2e - MYSQL_USER: userfeedback - MYSQL_PASSWORD: userfeedback - TZ: UTC - ports: - - 13307:3306 - volumes: - - ./volumes/mysql-for-e2e:/var/lib/mysql-for-e2e - networks: - - app_network - - # optional for email verification on creating user - smtp4dev: - image: rnwood/smtp4dev:v3 - restart: always - ports: - - 5080:80 - - 25:25 - - 143:143 - volumes: - - ./volumes/smtp4dev:/smtp4dev - networks: - - app_network - - # optional for better performance on searching feedbacks - opensearch-node: - image: opensearchproject/opensearch:2.14.0 - restart: always - container_name: opensearch-node - environment: - - cluster.name=opensearch-cluster - - node.name=opensearch-node - - discovery.type=single-node - - bootstrap.memory_lock=true - - 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m' - - plugins.security.disabled=true - - OPENSEARCH_INITIAL_ADMIN_PASSWORD=UserFeedback123!@# - ulimits: - memlock: - soft: -1 - hard: -1 - nofile: - soft: 65536 - hard: 65536 - volumes: - - ./volumes/opensearch:/usr/share/opensearch/data - ports: - - 9200:9200 - - 9600:9600 - networks: - - app_network - - # optional for opensearch - opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:2.14.0 - restart: always - container_name: opensearch-dashboards - ports: - - 5601:5601 - environment: - - 'OPENSEARCH_HOSTS=["http://opensearch-node:9200"]' - - 'DISABLE_SECURITY_DASHBOARDS_PLUGIN=true' - depends_on: - - opensearch-node - networks: - - app_network - -networks: - app_network: diff --git a/apps/cli/docker-compose.template.yml b/apps/cli/docker-compose.template.yml deleted file mode 100644 index b1728596d..000000000 --- a/apps/cli/docker-compose.template.yml +++ /dev/null @@ -1,33 +0,0 @@ -services: - web: - hostname: web - image: line/abc-user-feedback-web - restart: always - ports: - - 3000:3000 - extra_hosts: - - 'host.docker.internal:host-gateway' - environment: - - NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} - depends_on: - - api - - api: - hostname: api - image: line/abc-user-feedback-api - restart: always - ports: - - 4000:4000 - extra_hosts: - - 'host.docker.internal:host-gateway' - environment: - - JWT_SECRET=${JWT_SECRET} - - MYSQL_PRIMARY_URL=${MYSQL_PRIMARY_URL} - - BASE_URL=${BASE_URL} - - SMTP_HOST=${SMTP_HOST} - - SMTP_PORT=${SMTP_PORT} - - SMTP_SENDER=${SMTP_SENDER} - - SMTP_BASE_URL=${SMTP_BASE_URL} - - AUTO_MIGRATION=${AUTO_MIGRATION} - - MASTER_API_KEY=${MASTER_API_KEY} - - NODE_OPTIONS=${NODE_OPTIONS} diff --git a/apps/cli/eslint.config.js b/apps/cli/eslint.config.mjs similarity index 100% rename from apps/cli/eslint.config.js rename to apps/cli/eslint.config.mjs diff --git a/apps/cli/package.json b/apps/cli/package.json index 43d37d8f1..b4c2d9fa9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,29 +1,46 @@ { "name": "auf-cli", "version": "1.0.10", + "description": "Command line interface for ABC User Feedback", + "repository": "https://github.com/line/abc-user-feedback/tree/main/apps/cli", "bin": { - "auf-cli": "./dist/auf-cli.js" + "auf-cli": "./dist/index.js" }, + "author": "ABC User Feedback", + "keywords": [ + "auf", + "cli", + "command-line", + "tool", + "abc-user-feedback", + "VOC" + ], "scripts": { "build": "tsc", "clean": "git clean -xdf dist .turbo node_modules .cache", "format": "prettier --check . --ignore-path ../../.gitignore", "format:fix": "prettier --write --list-different .", "lint": "eslint", - "start": "node dist/auf-cli.js", - "start:dev": "ts-node bin/auf-cli.ts" + "start": "node dist/index.js", + "start:dev": "ts-node bin/index.ts", + "dev": "ts-node bin/index.ts" }, "dependencies": { "@types/prompts": "^2.4.9", "child_process": "^1.0.2", - "commander": "^14.0.0", - "js-toml": "^1.0.2" + "commander": "^14.0.2", + "js-toml": "^1.0.2", + "toml": "^3.0.0", + "yaml": "^2.8.2", + "zod": "^4.3.5" }, "devDependencies": { + "@types/node": "24.10.8", "@ufb/eslint-config": "workspace:*", "@ufb/prettier-config": "workspace:*", "@ufb/tsconfig": "workspace:*", - "ts-node": "^10.9.2" + "ts-node": "^10.9.2", + "typescript": "catalog:" }, "prettier": "@ufb/prettier-config" } diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index cdfdf747a..ec5172e9c 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -3,10 +3,8 @@ "compilerOptions": { "outDir": "./dist", "baseUrl": "./", - "paths": { - "@/*": ["./src/*"] - } + "resolveJsonModule": true }, - "include": ["bin"], + "include": ["."], "exclude": ["node_modules", "dist"] } diff --git a/apps/docs/.env.example b/apps/docs/.env.example new file mode 100644 index 000000000..f08cf14da --- /dev/null +++ b/apps/docs/.env.example @@ -0,0 +1,2 @@ +GOOGLE_SITE_VERIFICATION= +GOOGLE_ANALYTICS_TRACKING_ID= \ No newline at end of file diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore new file mode 100644 index 000000000..b2d6de306 --- /dev/null +++ b/apps/docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/apps/docs/README.md b/apps/docs/README.md new file mode 100644 index 000000000..0c6c2c27b --- /dev/null +++ b/apps/docs/README.md @@ -0,0 +1,41 @@ +# Website + +This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + +### Installation + +``` +$ yarn +``` + +### Local Development + +``` +$ yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +### Build + +``` +$ yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +### Deployment + +Using SSH: + +``` +$ USE_SSH=true yarn deploy +``` + +Not using SSH: + +``` +$ GIT_USER= yarn deploy +``` + +If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/apps/docs/docs/00-introduction/00-index.md b/apps/docs/docs/00-introduction/00-index.md new file mode 100644 index 000000000..c2028467e --- /dev/null +++ b/apps/docs/docs/00-introduction/00-index.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 1 +slug: / +--- + +# 환영합니다 + +환영합니다! 이 문서는 ABC User Feedback에 대한 포괄적인 가이드를 제공합니다. + +![ABC User Feedback](/assets/cover.png) + +## ABC User Feedback란? + +ABC User Feedback은 고객의 소리(VoC) 피드백을 효율적으로 수집, 분류, 관리하도록 설계된 독립형 웹 애플리케이션입니다. 피드백 태깅 시스템, 칸반 모드, 이슈 트래커, SSO 인증 등 다양한 기능을 제공합니다. 현재 1,000만 MAU를 보유한 서비스에서 활용되고 있습니다. + +

+

+ +## 주요 기능 + +- **피드백 태깅 시스템**: 주제별로 피드백을 분류하고 관리 +- **칸반 모드**: 이슈 그룹을 효율적으로 시각화하고 관리 +- **이슈 트래커 통합**: 상태 표시기로 이슈를 추적하고 외부 시스템과 통합 +- **싱글 사인온(SSO)**: 엔터프라이즈급 인증 요구사항을 지원하는 OAuth 인증 +- **역할 기반 접근 제어(RBAC)**: 세분화된 사용자 권한 관리 +- **대시보드**: 피드백 및 이슈에 대한 통계 데이터 시각화 + +## 시작하기 + +- [설치 가이드](/docs/02-developer-guide/01-installation/01-docker-hub-images.md) - Docker, CLI 도구 또는 수동 설치 방법 +- [튜토리얼](/docs/01-user-guide/01-getting-started.md) - 기본 사용법 가이드 + +## 지원 받기 + +질문이 있거나 도움이 필요하신가요? 다음 리소스를 활용하세요: + +- [GitHub Issues](https://github.com/line/abc-user-feedback/issues) - 버그 리포트 및 기능 요청 +- [GitHub Discussions](https://github.com/line/abc-user-feedback/discussions) - 커뮤니티 토론 diff --git a/apps/docs/docs/00-introduction/01-project-overview.md b/apps/docs/docs/00-introduction/01-project-overview.md new file mode 100644 index 000000000..429aaf025 --- /dev/null +++ b/apps/docs/docs/00-introduction/01-project-overview.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 1 +title: "프로젝트 개요" +description: "프로젝트 개요를 소개합니다." +--- + +# 프로젝트 개요 + +## ABC User Feedback이란? + +ABC User Feedback은 고객의 소리(Voice of Customer, VoC)를 효율적으로 수집, 분류 및 관리하기 위해 설계된 독립형 웹 애플리케이션입니다. 이 오픈소스 솔루션은 사용자 피드백을 체계적으로 관리하여 제품과 서비스 개선에 필요한 인사이트를 도출하는 데 중점을 두고 있습니다. + +현재 이 애플리케이션은 월간 활성 사용자(MAU) 1,000만 명 규모의 서비스에서 활용되고 있어, 대규모 피드백 처리에 대한 검증된 안정성을 갖추고 있습니다. + +## 핵심 가치 제안 + +ABC User Feedback은 다음 핵심 가치를 제공합니다: + +1. **중앙화된 피드백 관리**: 다양한 채널에서 수집된 사용자 피드백을 한 곳에서 관리 +2. **구조화된 분석**: 이슈 시스템을 통한 피드백 분류 및 추세 파악 +3. **이슈 추적**: 피드백에서 발견된 문제점을 이슈로 전환하여 추적 관리 +4. **데이터 기반 의사결정**: 대시보드를 통한 피드백 데이터 시각화 및 인사이트 도출 + +## 기술 스택 + +ABC User Feedback은 현대적인 웹 기술을 기반으로 구축되었습니다: + +- **프론트엔드**: [Next.js](https://nextjs.org/) - React 기반의 프론트엔드 프레임워크 +- **백엔드**: [NestJS](https://nestjs.com/) - TypeScript 기반의 확장 가능한 백엔드 프레임워크 +- **데이터베이스**: [MySQL v8](https://www.mysql.com/) - 안정적인 관계형 데이터베이스 +- **검색 엔진**: [OpenSearch v2.16](https://opensearch.org/) (선택 사항) - 대량의 피드백 데이터에 대한 고성능 검색 기능 + +## 아키텍처 개요 + +ABC User Feedback은 다음 주요 컴포넌트로 구성됩니다: + +1. **웹 관리자 인터페이스**: 피드백 관리, 이슈 추적, 대시보드 등 사용자 인터페이스를 제공하는 Next.js 기반 웹 애플리케이션 +2. **API 서버**: 데이터 처리, 비즈니스 로직, 인증 등을 담당하는 NestJS 기반 백엔드 서버 +3. **데이터베이스**: 피드백, 이슈, 사용자 정보 등을 저장하는 MySQL 데이터베이스 +4. **검색 엔진**: 대량의 피드백 데이터에 대한 고성능 검색을 제공하는 OpenSearch (선택 사항) +5. **SMTP 서버**: 계정 생성 시 이메일 인증, 비밀번호 재설정 등 사용자 인증 프로세스에 필요한 이메일 발송을 담당하는 컴포넌트 + +이 컴포넌트들은 Docker를 통해 컨테이너화되어 있어 쉽게 배포하고 확장할 수 있습니다. + +## 주요 사용 사례 + +ABC User Feedback은 다음 상황에서 특히 유용합니다: + +1. **제품 개선 프로세스**: 사용자 피드백을 수집하고 분석하여 제품 개선 방향 설정 +2. **고객 지원**: 사용자 문의와 이슈를 효율적으로 추적하고 관리 +3. **사용자 경험 최적화**: 사용자 의견을 기반으로 UX/UI 개선 +4. **품질 관리**: 버그 리포트와 기능 요청을 체계적으로 관리 +5. **데이터 기반 의사결정**: 사용자 피드백 통계를 활용한 전략적 의사결정 지원 + +## 차별화 요소 + +ABC User Feedback은 다음 특징으로 다른 피드백 관리 도구와 차별화됩니다: + +1. **완전한 오픈소스**: 상용 솔루션과 달리 완전히 무료로 사용 가능하며 커스터마이징 가능 +2. **엔터프라이즈급 기능**: SSO 인증, RBAC 등 기업 환경에 필요한 기능 제공 +3. **확장성**: 대규모 사용자 기반(1,000만 MAU)에서 검증된 성능 +4. **통합 용이성**: RESTful API와 웹훅으로 기존 시스템과 쉽게 통합 +5. **컨테이너화**: Docker 지원으로 간편한 배포 및 확장 + +## 다음 단계 + +ABC User Feedback을 시작하려면 다음 문서를 참조하세요: + +- [주요 기능](./02-key-features.md) - 상세한 기능 설명 +- [설치 가이드](/docs/02-developer-guide/01-installation/01-docker-hub-images.md) - 설치 방법 + +--- + +이 문서는 ABC User Feedback의 기본 개요를 제공합니다. 더 자세한 정보는 해당 섹션의 문서를 참조하세요. diff --git a/apps/docs/docs/00-introduction/02-key-features.md b/apps/docs/docs/00-introduction/02-key-features.md new file mode 100644 index 000000000..aff115ee0 --- /dev/null +++ b/apps/docs/docs/00-introduction/02-key-features.md @@ -0,0 +1,173 @@ +--- +sidebar_position: 3 +title: '주요 기능' +description: '주요 기능을 소개합니다.' +--- + +# 주요 기능 + +ABC User Feedback은 사용자 피드백을 효과적으로 수집, 관리 및 분석하기 위한 다양한 기능을 제공합니다. 이 문서에서는 핵심 기능들을 자세히 설명합니다. + +## 피드백 태깅 시스템 + +![Feedback Tag](/assets/01-feedback-tag.png) + +피드백 태깅 시스템은 대량의 사용자 피드백을 체계적으로 분류하고 관리하는 핵심 기능입니다. + +### 주요 특징 + +- **다중 이슈 할당**: 각 피드백에 여러 이슈를 할당하여 다차원적 분류 가능 +- **커스텀 이슈 생성**: 프로젝트 특성에 맞는 맞춤형 이슈 생성 및 관리 +- **이슈 기반 필터링**: 이슈별로 피드백을 필터링하여 특정 주제에 집중 +- **이슈 통계**: 이슈 사용 빈도 및 추세 분석을 통한 인사이트 도출 + +### 활용 방법 + +1. 관리자 패널에서 이슈 카테고리 및 이슈 생성 +2. 수신된 피드백에 관련 이슈 할당 +3. 이슈별 피드백 필터링 및 분석 +4. 이슈 사용 패턴을 통한 주요 이슈 및 추세 파악 + +## 칸반 모드 + +![Issue Kanban](/assets/02-Issue-Kanban.png) + +칸반 모드는 이슈 그룹을 시각적으로 관리하고 워크플로우를 최적화하기 위한 기능입니다. + +### 주요 특징 + +- **직관적인 드래그 앤 드롭**: 이슈 상태 변경을 위한 간편한 인터페이스 +- **상태별 열 구성**: 이슈 진행 상태에 따른 열 구분 (예: 할 일, 진행 중, 완료) +- **작업 흐름 시각화**: 팀의 작업 프로세스 및 진행 상황 한눈에 파악 +- **작업 부하 관리**: 각 상태별 이슈 수를 통한 작업 부하 모니터링 +- **필터 및 정렬**: 다양한 기준으로 칸반 보드 내 이슈 필터링 및 정렬 + +### 활용 방법 + +1. 칸반 모드 뷰 선택 +2. 상태별 이슈 확인 및 관리 +3. 드래그 앤 드롭으로 이슈 상태 변경 +4. 팀 작업 흐름 최적화 및 병목 현상 식별 + +## 이슈 트래커 연동 + +![Issue Tracker](/assets/03-issue-tracker.png) + +이슈 트래커 연동은 피드백에서 발견된 문제점이나 개선사항을 체계적으로 관리하기 위한 기능입니다. + +### 주요 특징 + +- **상태 표시기**: 이슈의 현재 상태를 시각적으로 표시 (신규, 진행 중, 해결됨 등) +- **외부 시스템 연동**: 이슈 트래커 시스템(JIRA)과 연결 가능 + +### 활용 방법 + +1. 피드백에서 이슈 생성 또는 이슈 메뉴에서 생성 +2. 외부 이슈 트래커 연결 설정 (선택 사항) +3. 이슈 세부 정보, 이슈 트랙킹 티켓 설정 +4. 이슈 진행 상황 모니터링 및 업데이트 +5. 해결 후 이슈 종료 + +## 싱글 사인온(SSO) + +![Single Sign-on](/assets/04-single-signon.png) + +싱글 사인온은 기업 환경에서의 인증 프로세스를 간소화하고 보안을 강화하는 기능입니다. + +### 주요 특징 + +- **OAuth 지원**: 다양한 OAuth 제공자를 통한 인증 지원 +- **기업 ID 통합**: 기존 기업 ID 시스템과의 원활한 통합 +- **중앙화된 사용자 관리**: 단일 인증 시스템을 통한 사용자 접근 관리 +- **보안 강화**: 다중 인증 및 기업 보안 정책 적용 가능 +- **간소화된 로그인 경험**: 사용자를 위한 추가 계정 생성 불필요 + +### 지원하는 SSO 제공자 + +- Google +- Custom (표준 OAuth 2.0 및 OpenID Connect 제공자) + +### 활용 방법 + +1. 관리자 설정에서 SSO 제공자 구성 +2. 인증 파라미터 및 리디렉션 URL 설정 +3. 사용자 속성 매핑 구성 +4. SSO 로그인 활성화 및 테스트 + +## 역할 기반 접근 제어(RBAC) + +![Role Management](/assets/05-role-management.png) + +역할 기반 접근 제어는 사용자의 권한을 효과적으로 관리하고 시스템 보안을 유지하는 기능입니다. + +### 주요 특징 + +- **사전 정의된 역할**: 관리자, 분석가, 뷰어 등 기본 역할 제공 +- **커스텀 역할 생성**: 조직 구조에 맞는 맞춤형 역할 및 권한 설정 +- **세분화된 권한 제어**: 기능별, 데이터별 접근 권한 설정 +- **역할 할당 관리**: 사용자별 역할 할당 및 변경 +- **권한 상속**: 계층적 권한 구조 지원 + +### 활용 방법 + +1. 관리자 패널에서 역할 관리 메뉴 접근 +2. 필요에 따라 새로운 역할 생성 또는 기존 역할 수정 +3. 사용자에게 적절한 역할 할당 +4. 역할별 권한 및 접근 범위 정기적 검토 + +## 대시보드 + +![Dashboard](/assets/06-dashboard.png) + +대시보드는 피드백 데이터를 시각화하여 중요한 인사이트를 한눈에 파악할 수 있게 해주는 기능입니다. + +### 주요 특징 + +- **실시간 통계**: 피드백 수, 이슈 수, 해결률 등 주요 지표 실시간 표시 +- **추세 분석**: 시간에 따른 피드백 및 이슈 추세 그래프 +- **이슈 분포**: 이슈별 피드백 분포 시각화 + +### 제공되는 차트 및 위젯 + +1. **피드백 요약 카드**: 총 피드백 수, 신규 피드백, 처리된 피드백 등 주요 지표 +2. **시계열 그래프**: 일별/주별/월별 피드백 추세 +3. **이슈 상태 도넛 차트**: 이슈 상태별 분포 + +### 활용 방법 + +1. 대시보드 페이지 접속 +2. 기간 및 필터 설정을 통한 데이터 범위 조정 +3. 주요 지표 및 추세 분석 +4. 인사이트 기반 의사결정 및 액션 아이템 도출 + +## 추가 기능 + +ABC User Feedback은 위에서 설명한 주요 기능 외에도 다음과 같은 추가 기능을 제공합니다: + +### API 통합 + +- RESTful API를 통한 외부 시스템 연동 +- 프로그래매틱 피드백 수집 및 관리 + +### 웹훅 + +- 주요 이벤트 발생 시 외부 시스템에 알림 +- 자동화된 워크플로우 구축 지원 + +### 이미지 저장 통합 + +- S3 호환 스토리지를 통한 사용자 제출 이미지 관리 +- 피드백에 스크린샷 및 이미지 첨부 기능 + +### 데이터 내보내기 + +- CSV, Excel 형식으로 피드백 데이터 내보내기 + +### 다국어 지원 + +- 다양한 언어로 인터페이스 제공 +- 국제적 팀을 위한 다국어 피드백 관리 + +--- + +이 문서는 ABC User Feedback의 주요 기능을 개괄적으로 설명합니다. 각 기능에 대한 더 자세한 사용법은 [사용자 가이드](/user-guide/getting-started) 섹션을 참조하세요. diff --git a/apps/docs/docs/00-introduction/_category_.json b/apps/docs/docs/00-introduction/_category_.json new file mode 100644 index 000000000..ccd04cb9d --- /dev/null +++ b/apps/docs/docs/00-introduction/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 1, + "label": "소개" +} diff --git a/apps/docs/docs/01-user-guide/01-getting-started.md b/apps/docs/docs/01-user-guide/01-getting-started.md new file mode 100644 index 000000000..e58a1de5c --- /dev/null +++ b/apps/docs/docs/01-user-guide/01-getting-started.md @@ -0,0 +1,271 @@ +--- +title: 시작하기 +description: ABC User Feedback 설치 후 초기 설정부터 첫 피드백 수집까지 시스템을 시작하는 방법을 설명합니다. +sidebar_position: 1 +--- + +# 시작하기 + +ABC User Feedback을 처음 설치한 후, 시스템을 사용하기 위해서는 초기 설정이 필요합니다. 이 문서에서는 테넌트 생성부터 첫 피드백 수집까지 전체 과정을 단계별로 안내합니다. + +--- + +## 초기 설정 개요 + +ABC User Feedback을 시작하기 위해서는 다음 순서로 설정을 진행합니다: + +1. **테넌트 및 관리자 계정 생성** +2. **첫 로그인 및 프로필 설정** +3. **프로젝트 생성** +4. **채널 생성 및 필드 설정** +5. **API 키 발급** +6. **첫 피드백 수집 테스트** + +--- + +## 시스템 접속 + +ABC User Feedback 설치가 필요한 경우, [Docker Hub 이미지를 사용한 설치](/docs/02-developer-guide/01-installation/01-docker-hub-images.md)를 먼저 진행하세요. + +설치를 완료했다면, 웹 브라우저를 통해 ABC User Feedback에 접속합니다: + +``` +http://localhost:3000 +``` + +> 포트나 도메인을 변경한 경우, 설정에 맞는 주소를 입력하세요. + +--- + +## 테넌트 및 관리자 계정 생성 + +![member-register.png](/img/tenant.png) + +처음 접속하면 **테넌트 생성 및 관리자 계정 등록** 화면이 표시됩니다. + +### Step 1: 테넌트 정보 입력 + +테넌트 이름의 이름을 설정합니다. +테넌트 이름을 입력한 후 **Next** 버튼을 클릭합니다. + +> 이 테넌트 이름은 로그인 UI에 표시됩니다. + +### Step 2: 관리자 계정 생성 + +시스템의 첫 관리자 계정을 생성합니다. + +1. 관리자 계정의 이메일을 입력한 후 **Request Code** 버튼을 클릭합니다. +2. 이메일함에서 인증 코드를 확인하여 입력합니다 +3. **Verify** 버튼을 클릭합니다 +4. 인증이 완료되면 비밀번호를 설정합니다. + +:::info 비밀번호 요구사항 + +- **8자 이상** +- **영문자 포함** (A–Z, a–z) +- **특수문자 포함** (예: `@`, `#`, `!`) +- **연속 문자 금지** (예: `aa`, `11`) + +> **예시**: ✅ `MyCompany2024!`, ❌ `12345678`, `password` + +::: + +테넌트와 관리자 계정 생성이 완료되면 확인 화면이 표시됩니다. + +**다음 단계**: **확인** 버튼을 클릭하여 로그인 화면으로 이동합니다. + +--- + +## 로그인하기 + +생성한 관리자 계정으로 첫 로그인을 진행합니다. + +1. **Email**: 앞에서 등록한 관리자 이메일을 입력합니다 +2. **Password**: 설정한 비밀번호를 입력합니다 +3. **Sign In** 버튼을 클릭합니다 + +--- + +## 첫 프로젝트 생성 + +로그인하면 프로젝트 생성 마법사가 자동으로 시작됩니다. + +### 시스템 구조 이해 + +ABC User Feedback은 다음과 같은 계층 구조를 가집니다: + +``` +테넌트 (조직) + └── 프로젝트 (제품/서비스 단위) + └── 채널 (피드백 수집 경로) +``` + +### Step 1: 프로젝트 기본 정보 + +| 항목 | 설명 | 예시 | +| --------------- | ----------------------------------- | -------------------------- | +| **Name** | 프로젝트 이름 | `모바일 앱`, `웹 서비스` | +| **Description** | 프로젝트 설명 (선택) | `고객 피드백 수집 및 분석` | +| **Time Zone** | 시간 기준 (대시보드 및 통계에 영향) | `Asia/Seoul` | + +**완료 후**: 정보를 입력한 후 **Next** 버튼을 클릭합니다. + +### Step 2: 팀 멤버 초대 (선택) + +이 단계에서는 프로젝트에 팀 멤버를 초대할 수 있습니다. 지금 건너뛰어도 나중에 언제든 추가할 수 있습니다. + +> 팀 멤버 관리에 대한 자세한 내용은 [프로젝트 관리](./02-project-management.md) 문서를 참고하세요. + +### Step 3: API 키 생성 (선택) + +외부 시스템과 연동할 API 키를 미리 생성할 수 있습니다. + +> API Key에 대해 자세한 내용은 [API Key 설정](./07-settings/02-api-key-management.md) 문서를 참고하세요. + +### 프로젝트 생성 완료 + +모든 정보를 입력하면 프로젝트 생성이 완료됩니다. + +**다음 단계 선택**: + +- **Create Channel**: 바로 채널을 생성해 피드백 수집 시작 +- **Skip for Now**: 나중에 채널 생성 + +--- + +## 첫 채널 생성 + +프로젝트 생성 후, 피드백을 실제로 수집하려면 **채널**을 생성해야 합니다. + +### 채널 개념 이해 + +채널은 **피드백 수집 경로**를 의미합니다: + +- 웹사이트 문의폼 +- 모바일 앱 내 피드백 +- 고객센터 VOC +- 설문조사 응답 + +### Step 1: 채널 기본 정보 + +| 항목 | 설명 | 예시 | +| ---------------------------------- | --------------------------------------------- | ---------------------- | +| **Name** | 채널 이름 | `웹 피드백`, `앱 리뷰` | +| **Description** | 채널 설명 (선택) | `웹사이트 사용자 의견` | +| **Maximum Feedback Search Period** | 피드백 검색 가능 기간 (30/90/180/365일, 전체) | `90일` | + +**완료 후**: 정보를 입력한 후 **Next** 버튼을 클릭합니다. + +### Step 2: 필드 설정 + +채널에서 수집할 데이터 구조를 정의합니다. + +#### 기본 제공 필드 + +시스템에서 자동으로 생성되는 필드들: + +| 필드명 | 형식 | 속성 | 설명 | +| ----------- | ----------- | --------- | ---------------- | +| `id` | number | Read Only | 피드백 고유 ID | +| `createdAt` | date | Read Only | 생성 시간 | +| `updatedAt` | date | Read Only | 수정 시간 | +| `issues` | multiSelect | Editable | 연결된 이슈 목록 | + +#### 커스텀 필드 추가 + +실제 피드백 수집을 위해 커스텀 필드를 추가합니다: + +1. **Add Field** 버튼 클릭 +2. 필드 정보 입력: + +| 항목 | 설명 | 예시 | +| ---------------- | --------------------------------------------------------- | -------------------------------------------------------------------------- | +| **Key** | 고유 식별자 (영문 대/소문자, 숫자, `_`) | `message`, `rating` | +| **Display Name** | UI에 표시될 이름 | `피드백 내용` | +| **Format** | 데이터 형식 | `text`,`keyword`,`number`,`date`,`select`,`multiSelect`,`images`,`aiField` | +| **Property** | `Editable` (UI에서 수정 가능) / `Read Only` (수정 불가능) | `Editable` | +| **Status** | `Active` / `Inactive` | `Active` | + +> 필드 정보에 대해 자세한 내용은 [필드 설정](./03-channel-management.md) 문서를 참고하세요. + +#### 권장 기본 필드 구성 + +첫 채널에는 다음 필드들을 추가하는 것을 권장합니다: + +| Key | Display Name | Format | 설명 | +| ----------- | ------------- | ------- | ------------- | +| `message` | 피드백 내용 | text | 사용자 피드백 | +| `userEmail` | 사용자 이메일 | keyword | 연락처 (선택) | +| `rating` | 만족도 | number | 1-5점 평가 | + +### 필드 미리보기 + +필드 설정을 완료한 후 **Preview** 버튼으로 피드백 입력 화면을 미리 확인할 수 있습니다. + +**완료 후**: **Complete** 버튼으로 채널 생성을 완료합니다. + +### 채널 생성 완료 + +**다음 단계**: **Start** 버튼을 클릭하여 피드백 수집을 시작합니다. + +--- + +## 첫 피드백 수집 테스트 + +채널 생성이 완료되면 실제로 피드백을 수집해볼 수 있습니다. + +### API를 통한 피드백 등록 + +생성한 API 키를 사용해 첫 피드백을 등록해봅시다. + +#### API 요청 예시 + +```bash +curl -X POST http://localhost:4000/api/projects/1/channels/1/feedbacks \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: YOUR_API_KEY" \ + -d '{ + "message": "앱 실행 속도가 느려요", + "userEmail": "user@example.com", + "rating": 3 + }' +``` + +> `YOUR_API_KEY`는 앞에서 생성한 실제 API 키로 대체하세요. + +#### 성공 응답 확인 + +API 요청이 성공하면 다음과 같은 응답을 받습니다: + +```json +{ + "id": 1 +} +``` + +### 피드백 확인하기 + +등록한 피드백을 웹 인터페이스에서 확인해봅시다. + +1. 상단 메뉴에서 **Feedback** 탭 클릭 +2. 피드백 목록에서 등록된 피드백 확인 +3. 피드백을 클릭하여 상세 정보 확인 + +### 첫 이슈 생성 + +피드백을 바탕으로 이슈를 생성해봅시다. + +1. 피드백 상세 화면에서 **Issue** 섹션의 **`+` 버튼** 클릭 +2. 이슈 이름 입력 후 **Enter** 키 또는 **Create** 버튼 클릭 +3. 생성된 이슈 확인 + +## 다음 단계 안내 + +기본 설정과 첫 피드백 수집이 완료되었습니다! + +## 관련 문서 + +- [프로젝트 관리](./02-project-management.md) - 프로젝트 설정 및 팀 관리 +- [채널 관리](./03-channel-management.md) - 채널 및 필드 고급 설정 +- [피드백 관리](./04-feedback-management.md) - 피드백 분석 및 관리 +- [API 연동](/docs/02-developer-guide/02-api-integration.md) - API 사용법 상세 가이드 diff --git a/apps/docs/docs/01-user-guide/02-project-management.md b/apps/docs/docs/01-user-guide/02-project-management.md new file mode 100644 index 000000000..d1d3c9a83 --- /dev/null +++ b/apps/docs/docs/01-user-guide/02-project-management.md @@ -0,0 +1,347 @@ +--- +title: 프로젝트 +description: ABC User Feedback에서 프로젝트를 생성, 설정, 관리하고 팀 멤버의 역할과 권한을 설정하는 방법을 설명합니다. +sidebar_position: 2 +--- + +# 프로젝트 + +ABC User Feedback에서 **프로젝트**는 피드백을 수집하고 분석하는 가장 기본적인 단위입니다. 프로젝트 생성부터 팀 관리, 권한 설정에 대한 기능을 다룹니다. + +--- + +## 프로젝트 개요 + +프로젝트는 다음 계층 구조를 가집니다: + +``` +테넌트 + └── 프로젝트 (여러 개 가능) + ├── 채널 (여러 개 가능) + ├── 멤버 및 역할 + ├── 이슈 트래커 연동 + ├── 웹훅 연동 + ├── AI 기능 + └── API 키 +``` + +각 프로젝트는 여러 채널을 포함하는 관리 단위로, 팀 멤버와 역할, 이슈 트래커 연동, 외부 시스템 연동 등을 독립적으로 설정하고 운영할 수 있습니다. + +--- + +## 프로젝트 생성하기 + +### 접근 권한 + +프로젝트 생성은 **Super 유저**만 가능합니다. 일반 사용자는 기존 프로젝트에 멤버로 초대받아 참여할 수 있습니다. + +> Super 유저 권한이 필요한 경우 시스템 관리자에게 문의하시기 바랍니다. + +### 접근 방법 + +새 프로젝트를 생성하는 방법은 두 가지입니다: + +1. **첫 로그인 시**: 프로젝트 생성 마법사가 자동으로 시작됩니다 +2. **추가 프로젝트**: 좌측 사이드바 상단의 **Create Project** 버튼을 클릭합니다 + +### Step 1: 프로젝트 기본 정보 + +![create-project-1](/img/project/1.png) + +프로젝트 생성 시 다음 정보를 입력합니다: + +| 항목 | 설명 | 예시 | +| --------------- | ------------------------------------------------------ | -------------------------------------- | +| **Name** | 프로젝트의 이름 (필수) | `모바일 앱`, `고객센터`, `Beta 서비스` | +| **Description** | 간단한 설명 (선택) | `iOS/Android 앱 사용자 피드백 수집` | +| **Time Zone** | 피드백 시간 기준 및 리포트 기준 시간대로 사용됨 (필수) | `Asia/Seoul` | + +> 시간대는 **대시보드 통계**에 영향을 줍니다. + +**완료 후**: 모든 정보를 입력한 뒤 **Next** 버튼을 클릭합니다. + +### Step 2: 팀 멤버 추가 (선택) + +![create-project-2](/img/project/2.png) + +이 단계는 **건너뛸 수 있습니다**. 나중에 프로젝트 설정에서 언제든 추가 가능합니다. + +#### 멤버 추가 방법 + +1. 우측 상단의 **Register Member** 버튼을 클릭합니다 +2. 다음 항목을 입력합니다: + - **Email**: 시스템에 등록된 사용자 선택 + - **Role**: Admin, Editor, Viewer 중 선택 + +> 커스텀 역할을 사용하고 싶다면 **Role Management** 버튼을 눌러 추가 설정이 가능합니다. + +**완료 후**: 멤버 목록을 확인하고 **Next**를 클릭합니다. + +### Step 3: API 키 발급 (선택) + +![create-project-3](/img/project/3.png) + +API 키는 외부 시스템에서 피드백을 수집할 때 사용됩니다. 설정 메뉴에서 나중에 발급 가능하므로 지금 생략해도 됩니다. + +#### 키 생성 방법 + +1. 우측 상단 **Create API Key** 버튼을 클릭합니다 +2. 키가 자동 생성되어 목록에 표시됩니다 +3. 생성된 키를 복사해 안전한 장소에 저장합니다 + +### 프로젝트 생성 완료 + +![create-project-4](/img/project/4.png) + +모든 단계를 완료하면 **요약 화면**이 나타납니다: + +- 프로젝트 정보: 이름, 설명, 시간대 +- 멤버 목록 +- 생성된 API 키 +- 역할 설정 상태 + +#### 다음 단계 + +- 바로 채널을 만들고 피드백 수집을 시작하려면 **Create Channel** 버튼을 클릭합니다 +- 또는 **Later** 버튼으로 나중에 생성할 수 있습니다 + +--- + +## 프로젝트 설정 관리 + +![project-setting.png](/img/project/project-setting.png) + +### 접근 방법 + +프로젝트 설정을 변경하려면: + +1. 상단 메뉴에서 **Settings** 클릭 +2. 좌측 메뉴에서 **Project Setting** 선택 + +### 기본 정보 수정 + +다음 항목들을 언제든 수정할 수 있습니다: + +| 항목 | 설명 | 주의사항 | +| --------------- | --------------------------------------- | ---------------------------------------- | +| **Name** | 프로젝트 이름 | 팀원들에게 표시되는 이름 | +| **Description** | 설명 (선택사항) | 프로젝트 목적 명시 | +| **Time Zone** | 통계 및 시간 관련 데이터 기준 시각 설정 | 변경 시 기존 데이터에는 영향을 주지 않음 | + +**저장 방법**: 수정 후 우측 상단의 **Save** 버튼을 클릭합니다. + +### 시간대 변경 시 주의사항 + +- 기존 피드백/이슈의 시간 정보에는 영향을 주지 않습니다 +- 변경 후 대시보드 통계에 데이터 불일치가 발생할 수 있습니다. + +### 프로젝트 삭제 + +#### 삭제 절차 + +프로젝트를 완전히 삭제하려면: + +1. Project Setting 화면 하단의 **Delete Project** 버튼을 클릭합니다 +2. 확인 팝업에서 프로젝트 이름을 정확히 입력합니다 +3. **Delete** 버튼으로 최종 확정합니다 + +#### 삭제 시 유의사항 + +- 해당 프로젝트 내 **모든 피드백, 이슈, 설정이 영구 삭제**됩니다 +- **되돌릴 수 없으므로** 사전 백업 또는 Export를 권장합니다 +- 삭제 시 연결된 채널 및 API 키도 함께 제거됩니다 + +--- + +## 멤버 관리 + +![member-setting.png](/img/project/member-setting.png) + +### 멤버 목록 확인 + +현재 프로젝트에 참여한 멤버를 확인하려면: + +1. 상단 메뉴에서 **Settings** 클릭 +2. 좌측 메뉴에서 **Member Management** 선택 + +멤버 목록에는 다음 정보가 표시됩니다: + +| 항목 | 설명 | +| ---------- | ------------------------- | +| Email | 계정 이메일 | +| Name | 사용자 이름 (프로필 기준) | +| Department | 소속 부서 | +| Role | 프로젝트 내 역할 | +| Joined | 프로젝트 참여일 | + +### 새 멤버 초대 + +![member-register.png](/img/project/member-register.png) + +#### 초대 절차 + +1. **Register Member** 버튼을 클릭합니다 +2. 초대 정보를 입력합니다: + +| 항목 | 설명 | +| --------- | -------------------------------------- | +| **Email** | 초대할 사용자의 이메일 | +| **Role** | 부여할 역할 (Admin, Editor, Viewer 등) | + +3. **Invite** 버튼을 클릭하여 초대를 완료합니다 + +### 멤버 정보 수정 + +기존 멤버의 정보를 수정하려면: + +1. 멤버 목록에서 수정하려는 멤버 행을 클릭합니다 +2. 팝업에서 다음 항목을 Role을 수정할 수 있습니다: +3. **Save** 버튼으로 변경사항을 저장합니다 + +### 멤버 제거 + +멤버를 프로젝트에서 제거하려면: + +1. 멤버 수정 팝업에서 하단의 **삭제** 버튼을 클릭합니다 +2. 확인 메시지에서 **확인**을 클릭합니다 + +> 멤버를 제거해도 해당 사용자가 작성한 피드백/이슈 기록은 그대로 유지되며, 프로젝트 접근 권한만 제거됩니다. + +--- + +## 역할 및 권한 관리 + +![role-setting.png](/img/project/role-setting.png) + +### 기본 제공 역할 + +시스템에서는 다음 기본 역할을 제공합니다: + +| 역할 | 권한 요약 | +| ---------- | ---------------------------------------------- | +| **Admin** | 모든 기능 접근 가능. 프로젝트 삭제 포함 | +| **Editor** | 피드백/이슈 생성, 수정, 삭제 가능. 설정은 불가 | +| **Viewer** | 조회만 가능. 수정, 삭제, 설정 접근 불가 | + +### 커스텀 역할 생성 + +![role-create.png](/img/project/role-create.png) + +더 세분화된 권한이 필요한 경우 커스텀 역할을 생성할 수 있습니다: + +1. Member Management 화면에서 **Role Management** 링크를 클릭합니다 +2. **Create Role** 버튼을 클릭합니다 +3. 역할 이름과와 권한을 입력합니다: + +### 권한 설정 + +각 역할에 대해 다음과 같은 기능별 권한을 설정할 수 있습니다: + +#### 피드백 권한 + +| 권한 항목 | 설명 | +| ----------------------------------- | ----------------------- | +| **Download Feedback** | 피드백 데이터 다운로드 | +| **Edit Feedback** | 피드백 편집 | +| **Delete Feedback** | 피드백 삭제 | +| **Attach/Detach Issue in Feedback** | 피드백과 이슈 연결/해제 | + +#### 이슈 권한 + +| 권한 항목 | 설명 | +| ---------------- | --------- | +| **Create Issue** | 이슈 생성 | +| **Edit Issue** | 이슈 편집 | +| **Delete Issue** | 이슈 삭제 | + +#### 프로젝트 관리 + +| 권한 항목 | 설명 | +| --------------------- | ------------------ | +| **Edit Project Info** | 프로젝트 정보 편집 | +| **Delete Project** | 프로젝트 삭제 | + +#### 멤버 관리 + +| 권한 항목 | 설명 | +| ------------------------- | ------------------ | +| **Read Project Member** | 프로젝트 멤버 조회 | +| **Create Project Member** | 프로젝트 멤버 초대 | +| **Edit Project Member** | 프로젝트 멤버 편집 | +| **Delete Project Member** | 프로젝트 멤버 삭제 | + +#### 역할 관리 + +| 권한 항목 | 설명 | +| ----------------------- | ------------------ | +| **Read Project Role** | 프로젝트 역할 조회 | +| **Create Project Role** | 프로젝트 역할 생성 | +| **Edit Project Role** | 프로젝트 역할 편집 | +| **Delete Project Role** | 프로젝트 역할 삭제 | + +#### API 키 관리 + +| 권한 항목 | 설명 | +| ------------------ | ----------- | +| **Read API Key** | API 키 조회 | +| **Create API Key** | API 키 생성 | +| **Edit API Key** | API 키 편집 | +| **Delete API Key** | API 키 삭제 | + +#### 이슈 트래커 + +| 권한 항목 | 설명 | +| ---------------------- | ---------------- | +| **Read Issue Tracker** | 이슈 트래커 조회 | +| **Edit Issue Tracker** | 이슈 트래커 설정 | + +#### 웹훅 관리 + +| 권한 항목 | 설명 | +| ------------------ | --------- | +| **Read Webhook** | 웹훅 조회 | +| **Create Webhook** | 웹훅 생성 | +| **Edit Webhook** | 웹훅 편집 | +| **Delete Webhook** | 웹훅 삭제 | + +#### AI 및 채널 설정 + +| 권한 항목 | 설명 | +| ---------------------- | ------------ | +| **Read Generative AI** | AI 설정 조회 | +| **Edit Generative AI** | AI 설정 편집 | + +#### 채널 관련 설정 + +| 권한 항목 | 설명 | +| ---------------------- | ---------------- | +| **Edit Channel Info** | 채널 정보 편집 | +| **Delete Channel** | 채널 삭제 | +| **Read Field** | 필드 조회 | +| **Edit Field** | 필드 편집 | +| **Read Image Setting** | 이미지 설정 조회 | +| **Edit Image Setting** | 이미지 설정 편집 | +| **Create Channel** | 새로운 채널 생성 | + +### 권한 설정 팁 + +#### 보안 모범 사례 + +- **최소 권한 원칙**: 업무에 필요한 최소한의 권한만 부여 +- **정기 검토**: 팀 변경이나 퇴사자 발생 시 권한 점검 +- **Admin 역할 제한**: 관리자는 가능한 한 적은 수로 유지 + +### 역할 수정 및 삭제 + +- **수정**: 역할 목록에서 원하는 항목을 클릭하여 이름과 권한을 수정할 수 있습니다 +- **삭제**: 사용 중이지 않은 역할은 **Delete** 버튼으로 삭제 가능합니다 + +> **주의**: Admin 역할은 항상 하나 이상 존재해야 하며, 삭제할 수 없습니다. + +--- + +## 관련 문서 + +- [채널 관리](./03-channel-management.md) - 채널 생성 및 필드 설정 +- [피드백 관리](./04-feedback-management.md) - 피드백 수집 및 분석 +- [API 연동](/docs/02-developer-guide/02-api-integration.md) - API 키 사용법 diff --git a/apps/docs/docs/01-user-guide/03-channel-management.md b/apps/docs/docs/01-user-guide/03-channel-management.md new file mode 100644 index 000000000..6d393bf8e --- /dev/null +++ b/apps/docs/docs/01-user-guide/03-channel-management.md @@ -0,0 +1,237 @@ +--- +title: 채널 +description: ABC User Feedback에서 피드백 수집 채널을 생성, 설정, 관리하고 커스텀 필드 및 이미지 설정을 다루는 방법을 설명합니다. +sidebar_position: 3 +--- + +# 채널 + +**채널(Channel)** 은 피드백을 수집하는 경로 또는 목적에 따라 구분되는 단위입니다. 각 채널은 독립된 필드 구조, 이미지 설정, AI 기능을 가져 다양한 피드백 수집 시나리오에 맞게 설정할 수 있습니다. + +--- + +## 채널 개요 + +### 채널의 역할 + +채널은 다음 역할을 합니다: + +- **피드백 수집 경로 구분**: 웹, 앱, 고객센터, 설문조사 등 +- **데이터 구조 정의**: 채널별 고유한 필드 설정 +- **수집 정책 관리**: 이미지 허용, 검색 기간, 보안 설정 등 +- **분석 단위 제공**: 채널별 독립적인 통계 및 분석 + +--- + +## 채널 생성하기 + +### 접근 방법 + +새 채널을 생성하는 방법은 다음과 같습니다: + +1. **프로젝트 생성 직후**: 프로젝트 완료 화면에서 **Create Channel** 버튼 클릭 +2. **추가 채널 생성**: **Settings > Channel List**에서 **Create Channel** 버튼 클릭 + +### Step 1: 채널 기본 정보 + +![channel-create-1](/img/channel/1.png) + +| 항목 | 설명 | 예시 | +| ---------------------------------- | ---------------------------------------------------- | ---------------------------------- | +| **Name** | 채널명 (필수) | `웹 피드백`, `앱 리뷰`, `고객센터` | +| **Description** | 채널 간단한 설명 (선택) | `웹사이트 사용자 의견 수집` | +| **Maximum Feedback Search Period** | 피드백 검색 가능한 최대 기간 (30/90/180/365일, 전체) | `90일` | + +#### 피드백 검색 가능한 최대 기간 설정 시 주의사항 + +- **영향 범위**: 피드백 다운로드 기능에 직접적으로 영향을 줍니다 +- **다운로드 동작**: 설정된 검색 기간 내의 모든 피드백이 다운로드 대상이 됩니다 +- **성능 테스트**: 일일 피드백 수가 많은 경우, 다양한 기간으로 테스트하여 최적값을 찾는 것을 권장합니다 +- **점진적 조정**: 초기에는 짧은 기간으로 시작하여 필요에 따라 점진적으로 늘려가는 것이 안전합니다 + +**완료 후**: 정보를 입력한 후 **Next** 버튼을 클릭합니다. + +### Step 2: 필드 설정 + +![channel-create-2](/img/channel/2.png) + +채널에서 수집할 데이터 구조를 정의합니다. 이는 API 요청 구조와 피드백 테이블 구성에 직접적으로 영향을 줍니다. + +#### 기본 시스템 필드 + +모든 채널에 자동으로 포함되는 필드들: + +| Key | Format | 속성 | 설명 | +| ----------- | ----------- | --------- | ---------------- | +| `id` | number | Read Only | 피드백 고유 ID | +| `createdAt` | date | Read Only | 피드백 생성 시각 | +| `updatedAt` | date | Read Only | 피드백 수정 시각 | +| `issues` | multiSelect | Editable | 연결된 이슈 목록 | + +> 이 필드들은 삭제하거나 주요 속성을 수정할 수 없습니다. + +#### 커스텀 필드 추가 + +실제 비즈니스 요구사항에 맞는 필드를 추가합니다. + +1. **Add Field** 버튼을 클릭합니다 +2. 필드 정보를 입력합니다 + +| 항목 | 설명 | 예시 | +| ---------------- | -------------------------------------------------- | ------------------------------ | +| **Key** | 고유 식별자 (영문 대/소문자, 숫자, `_`) | `message`, `rating` | +| **Display Name** | UI에 표시될 이름 | `피드백 내용`, `사용자 이메일` | +| **Format** | 데이터 형식 (아래 표 참고) | `text`, `keyword`, `number` | +| **Property** | `Editable` (입력 가능) / `Read Only` (조회만 가능) | `Editable` | +| **Status** | `Active` / `Inactive` | `Active` | +| **Description** | 팀원이 이해하기 쉬운 설명 (선택) | `사용자가 입력한 피드백 내용` | + +### 필드 Format 종류 + +| Format | 설명 | 사용 예시 | API 예시 | +| ------------- | ----------------- | ---------------------------- | ------------------------ | +| `text` | 자유 텍스트 입력 | 피드백 내용, 상세 설명 | `"앱이 자꾸 멈춰요"` | +| `keyword` | 짧은 키워드/태그 | 버전 정보, 페이지명 | `"v1.2.3"` | +| `number` | 숫자 | 평점, 나이, 사용 시간 | `5` | +| `date` | 날짜 | 발생일, 만료일 | `"2024-03-01T00:00:00Z"` | +| `select` | 단일 선택 | 카테고리, 우선순위 | `"기능 요청"` | +| `multiSelect` | 다중 선택 | 태그, 관련 기능 | `["버그", "UI"]` | +| `images` | 이미지 URL 배열 | 스크린샷, 첨부 파일 | `["https://..."]` | +| `aiField` | AI 분석 결과 필드 | 감정 분석, 요약, 키워드 추출 | `"긍정"` | + +> **images 형식 관련**: 자세한 이미지 설정 방법은 [이미지 설정](/docs/01-user-guide/07-settings/06-image-setting.md) 문서를 참고하세요. +> +> **aiField 형식 관련**: AI 필드 설정 및 템플릿 구성 방법은 [AI 설정](/docs/01-user-guide/07-settings/05-ai-setting.md) 문서를 참고하세요. + +### 필드 구성 예시 + +#### 웹 피드백 채널 + +| Key | Display Name | Format | 용도 | +| ------------- | ------------ | ------- | ---------------------- | +| `message` | 피드백 내용 | text | 사용자 의견 | +| `userEmail` | 이메일 | keyword | 연락처 (선택) | +| `pageUrl` | 페이지 URL | keyword | 피드백 발생 위치 | +| `category` | 카테고리 | select | 버그/기능요청/개선사항 | +| `priority` | 우선순위 | select | 높음/보통/낮음 | +| `screenshots` | 스크린샷 | images | 문제 상황 캡처 | + +#### 모바일 앱 리뷰 채널 + +| Key | Display Name | Format | 용도 | +| ------------ | ------------ | ------- | ---------------- | +| `message` | 리뷰 내용 | text | 사용자 리뷰 | +| `rating` | 평점 | number | 1-5점 평가 | +| `appVersion` | 앱 버전 | keyword | 버그 추적용 | +| `deviceType` | 기기 타입 | select | iOS/Android | +| `crashLogs` | 크래시 로그 | text | 기술적 오류 정보 | + +### 필드 미리보기 + +필드 설정을 완료한 후 **Preview** 버튼으로 실제 피드백 입력 화면을 미리 확인할 수 있습니다. + +이 미리보기는 API 요청 시 필요한 필드 구조와 동일합니다. + +**완료 후**: **Next** 버튼으로 다음 단계로 진행합니다. + +### Step3. 채널 생성 완료 + +![create-channel-3](/img/channel/3.png) + +모든 단계를 완료하면 **요약 화면**이 나타납니다: + +- 채널 정보: 이름, 설명, 시간대 +- 필드 정보 + +--- + +## 필드 관리 + +![field-management.png](/img/channel/field-management.png) + +### 필드 수정 + +기존 필드를 수정하려면 필드 목록에서 수정하려는 필드 행을 클릭하여 정보를 수정합니다. + +> **주의**: `Key`와 `Format`은 생성 후 수정할 수 없습니다. 데이터 일관성을 위해 제한됩니다. + +### 필드 삭제 + +피드백 데이터의 무결성과 일관성을 보장하기 위해 **필드 삭제 기능은 제공하지 않습니다**. + +#### 삭제 대신 권장하는 방법 + +1. **Inactive 상태로 변경**: 필드를 비활성화하여 새로운 피드백 수집에서 제외 +2. **데이터 보존**: 기존에 수집된 피드백 데이터는 그대로 유지 +3. **필터링 활용**: 필드 목록에서 Active 필드만 표시하여 관리 효율성 확보 + +#### 완전 제거가 필요한 경우 + +필드를 완전히 제거해야 하는 상황에서는: + +- 채널 전체를 삭제하고 새로 생성하는 방법을 고려 +- 데이터 Export 후 새 구조로 마이그레이션 +- 개발팀과 상의하여 데이터베이스 레벨에서 처리 + +### 필드 상태 관리 + +#### Active / Inactive 전환 + +- **Active**: 피드백 수집 시 사용되는 필드 +- **Inactive**: 일시적으로 비활성화된 필드 (데이터는 보존) + +#### 필터링 옵션 + +상단 컨트롤로 다음 조건으로 필드를 필터링할 수 있습니다: + +- **Status**: `Active` / `Inactive` +- **Property**: `Editable` / `Read Only` + +--- + +## 채널 정보 관리 + +![channel-setting](/img/channel/channel-setting.png) + +### 채널 기본 정보 수정 + +생성된 채널의 기본 정보를 수정할 수 있습니다. + +#### 접근 방법 + +1. **Settings > Channel List > [채널 선택]** +2. **Channel Information** 탭 클릭 + +#### 수정 가능한 항목 + +| 항목 | 수정 가능 여부 | 주의사항 | +| ---------------------------------- | -------------- | ------------------------ | +| **Channel ID** | ❌ 불가능 | 시스템 내부 식별자 | +| **Channel Name** | ✅ 가능 | 팀원들에게 표시되는 이름 | +| **Description** | ✅ 가능 | 채널 목적 명시 | +| **Maximum Feedback Search Period** | ✅ 가능 | 성능에 영향을 줄 수 있음 | + +### 채널 삭제 + +더 이상 사용하지 않는 채널을 삭제할 수 있습니다. + +#### 삭제 절차 + +1. Channel Information 화면 하단의 **Delete Channel** 버튼 클릭 +2. 확인 팝업에서 채널 이름을 정확히 입력 +3. **Delete** 버튼으로 최종 확정 + +#### 삭제 시 유의사항 + +- 해당 채널의 **모든 피드백 데이터가 영구 삭제**됩니다 +- **되돌릴 수 없으므로** 사전 백업 또는 Export를 권장합니다 +- 관련된 API 키 설정도 함께 확인이 필요합니다 + +--- + +## 관련 문서 + +- [프로젝트 관리](./02-project-management.md) - 프로젝트 설정 및 권한 관리 +- [피드백 관리](./04-feedback-management.md) - 수집된 피드백 분석 및 활용 +- [API 연동](/docs/02-developer-guide/02-api-integration.md) - 외부 시스템과의 연동 방법 +- [AI 연동](/docs/01-user-guide/07-settings/05-ai-setting.md) - AI 기능 설정 diff --git a/apps/docs/docs/01-user-guide/04-feedback-management.md b/apps/docs/docs/01-user-guide/04-feedback-management.md new file mode 100644 index 000000000..b3d477e6c --- /dev/null +++ b/apps/docs/docs/01-user-guide/04-feedback-management.md @@ -0,0 +1,336 @@ +--- +title: 피드백 +description: ABC User Feedback에서 피드백을 생성, 조회, 분석 및 관리하는 방법을 설명합니다. +sidebar_position: 4 +--- + +# 피드백 + +피드백은 ABC User Feedback의 핵심 데이터입니다. 이 문서에서는 피드백 생성부터 분석, 관리까지 피드백과 관련된 모든 기능을 다룹니다. + +![feedback](/img/feedback/0.png) + +--- + +## 피드백 생성 + +피드백은 주로 외부 시스템(웹사이트, 모바일 앱, API 연동)으로 생성되지만, 관리자가 직접 생성할 수도 있습니다. + +### API를 통한 피드백 생성 + +가장 일반적인 피드백 생성 방법입니다. + +#### 기본 API 요청 구조 + +```bash +curl -X POST http://your-domain.com/api/v1/projects/{projectId}/channels/{channelId}/feedbacks \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: YOUR_API_KEY" \ + -d '{ + "message": "사용자 피드백 내용", + "userEmail": "user@example.com", + "category": "버그 신고" + }' +``` + +#### 채널 필드에 따른 요청 예시 + +각 채널의 필드 설정에 따라 요청 구조가 달라집니다: + +**웹 피드백 채널 예시**: + +```json +{ + "message": "로그인 버튼이 작동하지 않습니다", + "userEmail": "user@company.com", + "pageUrl": "https://example.com/login", + "category": "버그", + "priority": "높음", + "browserInfo": "Chrome 119.0.0" +} +``` + +**모바일 앱 채널 예시**: + +```json +{ + "message": "앱이 자주 멈춰요", + "rating": 2, + "appVersion": "v2.1.3", + "deviceType": "iOS", + "crashLogs": "Exception in thread main..." +} +``` + +#### 이미지가 포함된 피드백 + +이미지 URL 방식을 사용하는 경우: + +```json +{ + "message": "화면이 깨져서 보입니다", + "userEmail": "user@example.com", + "images": [ + "https://cdn.example.com/screenshot1.png", + "https://cdn.example.com/screenshot2.png" + ] +} +``` + +### 피드백 생성 확인 + +생성된 피드백은 즉시 피드백 목록에 표시됩니다. + +--- + +## 피드백 목록 접근 + +생성된 피드백들을 확인하고 관리하기 위해 피드백 목록에 접근합니다. + +### 접근 방법 + +1. 좌측 사이드바에서 원하는 **프로젝트**를 선택합니다 +2. 하단 채널 목록에서 원하는 **채널**을 클릭합니다 +3. 상단 메뉴에서 **Feedback** 탭을 선택합니다 + +### 피드백 테이블 구성 + +피드백 목록은 테이블 형태로 표시되며, 다음과 같은 기본 구조를 가집니다: + +| 컬럼 유형 | 설명 | 예시 | +| --------------- | -------------------------- | ---------------------------- | +| **디폴트 컬럼** | 모든 채널에 공통으로 표시 | ID, Created, Updated, Issue | +| **커스텀 컬럼** | 채널 필드 설정에 따라 표시 | Message, UserEmail, Category | + +--- + +## 피드백 필터링/정렬/뷰 옵션 + +대량의 피드백 데이터에서 원하는 정보를 빠르게 찾기 위한 다양한 도구들을 제공합니다. + +![feedback-option](/img/feedback/1.png) + +### 날짜 필터링 + +상단의 **Date** 버튼으로 조회 기간을 설정할 수 있습니다. + +#### 제공되는 기간 옵션 + +| 옵션 | 설명 | 사용 사례 | +| --------------- | ----------------------- | ---------------- | +| **오늘** | 당일 등록된 피드백 | 실시간 모니터링 | +| **어제** | 전일 피드백 | 일일 리뷰 | +| **지난 7일** | 최근 1주일 데이터 | 주간 분석 | +| **지난 30일** | 최근 1개월 데이터 | 월간 트렌드 파악 | +| **사용자 정의** | 시작일-종료일 직접 설정 | 특정 기간 분석 | + +### 필터 + +**Filter** 버튼을 클릭하면 다양한 조건으로 피드백을 필터링할 수 있습니다. + +![feedback-filter](/img/feedback/2.png) + +#### 필터 구조 + +``` +Where: 첫 번째 조건 +And: 모든 조건을 만족하는 피드백 +Or: 하나라도 만족하는 피드백 +``` + +> **주의**: `And`와 `Or`는 한 번에 혼합 사용할 수 없습니다. + +#### 필드별 필터 옵션 + +| 필드 유형 | 사용 가능한 연산자 | 예시 | +| --------------- | ------------------------------------ | ------------------------- | +| **text** | Contains (부분 일치) | message contains "버그" | +| **keyword** | Is (완전 일치) | category is "기능 요청" | +| **number** | Is (완전 일치) | rating == 3 | +| **select** | Is (완전 일치) | | +| **multiSelect** | Is (완전 일치), Contains (부분 일치) | | +| **aiField** | Contains (부분 일치) | | +| **date** | Is (완전 일치), Between (기간 일치) | created between 날짜 범위 | + +#### 필터 사용 예시 + +**멀티셀렉트 카테고리의 고급 검색**: + +``` +Where: category contains "버그" +And: priority is "높음" +``` + +**복수 이슈 연결된 피드백 찾기**: + +``` +Where: issues contains "로그인 이슈" +Or: issues contains "UI 개선" +``` + +**특정 카테고리의 4 평점 피드백 찾기**: + +``` +Where: category is "기능 요청" +And: rating is 4 +``` + +### 정렬 기능 + +테이블 헤더를 클릭하여 해당 컬럼 기준으로 정렬할 수 있습니다. Created 컬럼과 Updated 컬럼에 해당 기능이 제공됩니다. + +### 뷰 옵션 + +피드백 목록의 표시 방식을 사용자의 필요에 맞게 조정할 수 있습니다. + +#### Expand 기능 + +**Expand** 버튼을 클릭하면 테이블에서 각 피드백의 상세 내용을 미리 볼 수 있습니다. + +**활용 방법**: + +- 상세 패널을 열지 않고도 주요 내용 확인 +- 여러 피드백을 빠르게 훑어보기 +- 긴 텍스트 필드의 전체 내용 확인 + +#### 컬럼 표시/숨기기 + +테이블 상단의 **View** 버튼으로 표시할 컬럼을 선택할 수 있습니다. + +**기능**: + +- **필수 컬럼**: ID, Created는 항상 표시 (숨김 불가) +- **선택적 컬럼**: 커스텀 필드들을 개별적으로 표시/숨김 설정 +- **화면 최적화**: 필요한 정보만 표시하여 화면 공간 효율적 활용 + +**사용 팁**: + +``` +모니터링용: ID, Created, Message만 표시 +분석용: 모든 커스텀 필드 표시 +리뷰용: Message, Category, Priority 표시 +``` + +## 피드백 확인/수정/삭제 + +개별 피드백의 상세 정보를 확인하고 필요시 수정하거나 삭제할 수 있습니다. + +### 피드백 상세 보기 + +#### 접근 방법 + +피드백 테이블에서 **행을 클릭**하면 우측에 상세 보기 패널이 열립니다. + +![feedback-detail](/img/feedback/3.png) + +### 상세 패널 구성 + +상세 패널은 다음처럼 구성됩니다: + +#### 1. 기본 정보 영역 + +- **피드백 ID**: 고유 식별 번호 +- **생성 시간**: 최초 등록 일시 +- **수정 시간**: 마지막 변경 일시 +- **이슈**: 태깅된 이슈들 + +#### 2. 커스텀 필드 영역 + +채널에서 설정한 모든 커스텀 필드가 표시됩니다. + +### 피드백 수정 + +#### 편집 가능한 필드 + +상세 패널에서 **Edit** 버튼을 클릭하여 수정 모드로 전환할 수 있습니다. + +#### 수정 가능한 항목 + +| 항목 | 수정 가능 여부 | 주의사항 | +| --------------- | -------------- | ----------------------------------------------------------------- | +| **디폴트 필드** | ❌ 불가능 | ID, 생성일 등 | +| **커스텀 필드** | ✅ 가능 | 필드 설정의 Property에 따라 다름, Status가 Inactive일 경우 불가능 | + +#### 수정 완료 + +1. 필요한 정보를 수정합니다 +2. **Save** 버튼을 클릭합니다 +3. 수정 내용이 즉시 반영되며 "Updated" 시간이 갱신됩니다 + +### 이슈 연결 관리 + +#### 새 이슈 생성 + +1. 이슈 컬럼에 **+ 버튼**을 클릭합니다 +2. 이슈 이름을 입력하고 **Create** 옵션을 클릭 합니다 + +#### 기존 이슈 연결 + +1. 이슈 섹션의 **+ 버튼**을 클릭합니다 +2. 연결할 이슈의 이름을 입력합니다 +3. 드롭다운에서 연결할 이슈를 선택합니다 + +#### 이슈 연결 해제 + +1. 이슈 섹션의 **+ 버튼**을 클릭합니다 +2. 해제할 이슈를 선택합니다 + +### 피드백 삭제 + +#### 단일 피드백 삭제 + +1. 상세 패널 하단의 **Delete Feedback** 버튼을 클릭합니다 +2. 확인 대화상자에서 삭제를 승인합니다 + +#### 다중 피드백 삭제 + +1. 피드백 목록에서 **체크박스**를 통해 여러 피드백을 선택합니다 +2. 상단에 나타나는 **Delete Selected** 버튼을 클릭합니다 +3. 일괄 삭제를 확인합니다 + +#### 삭제 시 주의사항 + +- **복구 불가능**: 삭제된 피드백은 되돌릴 수 없습니다 +- **이슈 연결 해제**: 연결된 이슈는 그대로 유지되지만 연결이 해제됩니다 +- **통계 영향**: 대시보드 통계에서 해당 데이터가 제외됩니다 + +--- + +## 피드백 다운로드 + +수집된 피드백 데이터를 분석하거나 백업하기 위해 다양한 형식으로 내보낼 수 있습니다. + +### 다운로드 기능 접근 + +#### 전체 피드백 다운로드 + +1. 피드백 목록 상단의 **Export** 버튼을 클릭합니다 + +#### 필터된 피드백 다운로드 + +1. 원하는 조건으로 필터링을 적용합니다 +2. **Export** 버튼을 클릭하여 현재 필터 조건에 맞는 데이터만 다운로드합니다 + +#### 선택된 피드백 다운로드 + +1. 체크박스로 특정 피드백들을 선택합니다 +2. **Export Selected** 버튼을 클릭합니다 + +### 다운로드 형식 선택 + +Export 버튼 클릭 시 다운로드 형식을 선택할 수 있습니다. + +#### 지원되는 형식 + +| 형식 | 확장자 | 장점 | 권장 사용 사례 | +| --------- | ------- | ------------------------- | ------------------------- | +| **CSV** | `.csv` | 가볍고 호환성 우수 | Excel, Google Sheets 분석 | +| **Excel** | `.xlsx` | 서식 보존, 다중 시트 지원 | 상세 분석, 보고서 작성 | + +--- + +## 관련 문서 + +- [채널 관리](./03-channel-management.md) - 피드백 수집을 위한 채널 및 필드 설정 +- [이슈 관리](./05-issue-management.md) - 피드백에서 이슈 생성 및 관리 +- [API 연동](/docs/02-developer-guide/02-api-integration.md) - 외부 시스템에서 피드백 전송 방법 diff --git a/apps/docs/docs/01-user-guide/05-issue-management.md b/apps/docs/docs/01-user-guide/05-issue-management.md new file mode 100644 index 000000000..2934d10f9 --- /dev/null +++ b/apps/docs/docs/01-user-guide/05-issue-management.md @@ -0,0 +1,314 @@ +--- +title: 이슈 +description: ABC User Feedback에서 이슈를 생성, 관리하고 칸반/리스트 뷰로 효율적으로 추적하는 방법을 설명합니다. +sidebar_position: 5 +--- + +# 이슈 + +**이슈(Issue)** 는 피드백에서 발견된 문제점이나 개선사항을 체계적으로 관리하기 위한 핵심 기능입니다. 이슈 생성부터 카테고리 관리, 다양한 뷰 모드 활용까지 이슈 관리의 모든 기능을 다룹니다. + +![issue](/img/issue/1.png) + +--- + +## 이슈 개요 + +### 이슈의 역할 + +이슈는 다음 목적으로 사용됩니다: + +- **문제 추적**: 버그, 오류, 성능 문제 등을 체계적으로 관리 +- **기능 요청 관리**: 사용자 요청사항을 구조화하여 개발 계획에 반영 +- **개선사항 도출**: 피드백 분석을 통한 개선 포인트 식별 + +### 이슈 상태 + +각 이슈는 다음 상태를 가집니다: + +| 상태 | 설명 | 사용 시점 | +| --------------- | ----------- | ------------------------ | +| **New** | 새로 등록됨 | 이슈 최초 생성 | +| **On Review** | 검토 중 | 담당자가 검토 시작 | +| **In Progress** | 처리 중 | 실제 작업 진행 중 | +| **Resolved** | 해결 완료 | 문제 해결 및 완료 | +| **On Hold** | 일시 보류 | 추가 정보 대기 또는 연기 | + +--- + +## 이슈 생성/수정/삭제 + +### 이슈 생성 방법 + +이슈는 두 가지 방법으로 생성할 수 있습니다. + +#### 1. 피드백에서 이슈 생성 (권장) + +가장 일반적인 방법으로, 특정 피드백을 기반으로 이슈를 생성합니다. + +1. **Feedback** 탭에서 피드백을 클릭하여 상세 보기를 엽니다 +2. 우측 상세 패널의 **Issue** 섹션에서 **`+` 버튼**을 클릭합니다 +3. 이슈 이름을 입력하고 **Enter** 키를 누르거나 **Create** 옵션을 클릭합니다 + +#### 2. 이슈 목록에서 직접 생성 + +1. 상단 메뉴에서 **Issue** 탭을 클릭합니다 +2. 좌측 상단의 **+ Create Issue** 버튼을 클릭합니다 +3. 이슈 생성 대화상자에서 정보를 입력합니다: + +| 항목 | 설명 | 필수 여부 | 예시 | +| --------------- | ----------------------- | --------- | ------------------------ | +| **Title** | 이슈 제목 | 필수 | `로그인 버튼 오작동` | +| **Description** | 상세 설명 | 선택 | `특정 브라우저에서 발생` | +| **Category** | 이슈 분류 | 선택 | `버그` | +| **Status** | 초기 상태 (기본값: New) | 선택 | `New` | + +### 이슈 수정 + +생성된 이슈의 정보를 수정할 수 있습니다. + +#### 수정 방법 + +1. 이슈 목록에서 수정하려는 이슈를 클릭합니다 +2. 우측에 열리는 **Issue Details** 패널에서 **Edit** 버튼을 클릭합니다 +3. 편집 모드에서 다음 항목들을 수정할 수 있습니다: + +#### 수정 가능한 항목 + +| 항목 | 수정 가능 여부 | 설명 | +| --------------- | -------------- | ----------------------------- | +| **Title** | ✅ 가능 | 이슈 제목 | +| **Description** | ✅ 가능 | 상세 설명 | +| **Category** | ✅ 가능 | 이슈 분류 (드롭다운에서 선택) | +| **Status** | ✅ 가능 | 현재 진행 상태 | +| **Ticket** | ✅ 가능 | 외부 이슈 트래커 티켓 번호 | +| **ID** | ❌ 불가능 | 시스템 자동 생성 | +| **Created** | ❌ 불가능 | 생성 일시 | + +#### 저장 및 취소 + +- **Save** 버튼: 변경사항을 저장하고 수정 모드를 종료합니다 +- **Cancel** 버튼: 변경사항을 취소하고 원래 상태로 되돌립니다 + +### 외부 이슈 트래커 연동 + +외부 이슈 트래커(Jira 등)와 연동이 설정되어 있는 경우, 이슈에 외부 티켓을 연결할 수 있습니다. + +#### 티켓 연결 방법 + +1. 이슈 상세 패널에서 **Ticket** 필드에 외부 티켓 번호를 입력합니다 +2. 입력된 번호는 자동으로 외부 시스템 링크로 변환됩니다 + +> **참고**: 외부 이슈 트래커 연동은 **Settings > Issue Tracker Management**에서 사전 설정이 필요합니다. + +### 이슈 삭제 + +더 이상 필요하지 않은 이슈를 삭제할 수 있습니다. + +#### 삭제 방법 + +1. 이슈 상세 패널에서 **Delete** 버튼을 클릭합니다 + +2. 확인 대화상자에서 삭제를 승인합니다 + +#### 삭제 시 주의사항 + +- **복구 불가능**: 삭제된 이슈는 되돌릴 수 없습니다 +- **피드백 연결 해제**: 연결된 피드백들의 이슈 링크가 제거됩니다 +- **통계 영향**: 대시보드 이슈 통계에서 해당 데이터가 제외됩니다 + +--- + +## 칸반뷰 + +칸반뷰는 이슈들을 상태별 열로 구분하여 시각적으로 관리할 수 있는 보기 방식입니다. + +![issue-kanban](/img/issue/2.png) + +### 칸반뷰 접근 + +1. 상단 메뉴에서 **Issue** 탭을 클릭합니다 +2. 우측 상단에서 **Kanban** 뷰를 선택합니다 + +### 칸반 보드 구성 + +각 상태별로 열이 구성되어 있으며, 이슈들이 카드 형태로 표시됩니다. + +#### 칸반 열 구성 + +| 열 | 표시 정보 | 카드 개수 표시 | +| --------------- | ------------------ | -------------- | +| **New** | 새로 등록된 이슈들 | 상단에 숫자 | +| **On Review** | 검토 중인 이슈들 | 상단에 숫자 | +| **In Progress** | 진행 중인 이슈들 | 상단에 숫자 | +| **Resolved** | 해결 완료된 이슈들 | 상단에 숫자 | +| **On Hold** | 보류된 이슈들 | 상단에 숫자 | + +#### 이슈 카드 정보 + +각 이슈 카드에는 다음 정보가 표시됩니다: + +- **이슈 제목**: 클릭 시 상세 보기로 이동 +- **피드백 수**: 연결된 피드백 개수 (📝 아이콘과 함께) +- **카테고리**: 설정된 경우 하단에 표시 +- **외부 티켓**: 연결된 경우 티켓 번호 표시 + +### 드래그 앤 드롭 상태 변경 + +칸반뷰의 핵심 기능으로, 이슈 카드를 드래그하여 다른 열로 이동시켜 상태를 변경할 수 있습니다. + +#### 사용 방법 + +1. 이슈 카드를 마우스로 클릭하고 드래그합니다 +2. 원하는 상태 열 위로 이동시킵니다 +3. 마우스를 놓으면 상태가 자동으로 변경됩니다 + +### 칸반뷰 필터링 + +상단의 필터 기능을 사용하여 특정 조건의 이슈만 표시할 수 있습니다. + +#### 사용 가능한 필터 + +1. **Date** 필터: 특정 기간에 생성된 이슈만 표시 +2. **Filter** 버튼: 고급 필터 조건 설정 + +#### 필터 조건 예시 + +| 필터 유형 | 조건 예시 | 사용 사례 | +| ------------ | ----------------------- | -------------------------- | +| **Category** | Category = "버그" | 버그 이슈만 확인 | +| **Title** | Title contains "로그인" | 로그인 관련 이슈 찾기 | +| **Created** | Created >= 2024-03-01 | 특정 날짜 이후 생성된 이슈 | +| **Status** | Status != "Resolved" | 미해결 이슈만 표시 | + +### 칸반뷰 정렬 + +각 열 내에서 이슈 카드의 정렬 순서를 변경할 수 있습니다. + +#### 정렬 옵션 + +- **Created Date ↓**: 최신 생성 순 +- **Created Date ↑**: 오래된 순 +- **Feedback Count ↓**: 연결된 피드백 수 많은 순 + +--- + +## 리스트뷰 + +리스트뷰는 이슈들을 카테고리별로 그룹화하여 테이블 형태로 표시하는 보기 방식입니다. + +### 리스트뷰 접근 + +1. 상단 메뉴에서 **Issue** 탭을 클릭합니다 +2. 우측 상단에서 **List** 뷰를 선택합니다 + +### 리스트뷰 구성 + +카테고리별로 그룹화된 이슈들이 계층적으로 표시됩니다. + +#### 카테고리 그룹 + +각 카테고리는 접힘/펼침이 가능한 그룹으로 표시됩니다: + +- **그룹 헤더**: 카테고리 이름과 포함된 이슈 수 +- **접힘/펼침 화살표**: 그룹 내용 표시/숨김 토글 +- **"No Category"**: 카테고리가 지정되지 않은 이슈들 + +### 리스트뷰 필터링 + +칸반뷰와 동일한 필터 기능을 제공합니다. + +#### 필터 적용 방법 + +1. 상단의 **Date** 또는 **Filter** 버튼을 클릭합니다 +2. 원하는 조건을 설정합니다 +3. 필터링된 결과가 카테고리별로 그룹화되어 표시됩니다 + +#### 빈 카테고리 처리 + +필터링 결과 이슈가 없는 카테고리는 자동으로 숨겨집니다. + +### 리스트뷰 정렬 + +각 컬럼 헤더를 클릭하여 정렬할 수 있습니다. + +#### 정렬 동작 + +- **첫 번째 클릭**: 오름차순 정렬 ↑ +- **두 번째 클릭**: 내림차순 정렬 ↓ + +#### 각 정렬 내용 + +| 정렬 기준 | 사용 사례 | +| -------------------- | -------------------------- | +| **Created ↓** | 최신 이슈부터 확인 | +| **Feedback Count ↓** | 영향도가 큰 이슈 우선 처리 | +| **Status** | 상태별로 그룹화하여 확인 | + +--- + +## 이슈 카테고리 + +이슈 카테고리는 이슈를 분류하여 체계적으로 관리할 수 있게 해주는 기능입니다. + +### 카테고리의 목적 + +- **이슈 분류**: 버그, 기능 요청, 개선 사항 등으로 구분 +- **분석 용이성**: 카테고리별 이슈 발생 패턴 분석 + +### 기본 카테고리 예시 + +일반적으로 사용되는 카테고리 분류: + +| 카테고리 | 설명 | 우선순위 | 담당팀 예시 | +| ------------- | ---------------------- | -------- | ----------- | +| **버그** | 기능 오작동, 오류 | 높음 | 개발팀 | +| **기능 요청** | 새로운 기능 추가 요청 | 중간 | 기획팀 | +| **개선 사항** | 기존 기능 향상 | 중간 | UX팀 | +| **성능** | 속도, 안정성 문제 | 높음 | 인프라팀 | +| **UI/UX** | 사용자 인터페이스 문제 | 낮음 | 디자인팀 | +| **문서** | 도움말, 가이드 관련 | 낮음 | 기술문서팀 | + +### 카테고리 관리 + +#### 카테고리 추가 + +이슈 상세 패널에서 새로운 카테고리를 추가할 수 있습니다: + +1. 이슈 상세 패널의 **Category** 필드에서 **Add** 버튼을 클릭합니다 +2. 새 카테고리 이름을 입력합니다 +3. **Enter** 키를 누르거나 확인 버튼을 클릭합니다 + +#### 카테고리 할당 + +기존 이슈에 카테고리를 할당하거나 변경할 수 있습니다: + +1. 이슈 상세 패널에서 **Edit** 버튼을 클릭합니다 +2. **Category** 드롭다운에서 원하는 카테고리를 선택합니다 +3. **Save** 버튼으로 변경사항을 저장합니다 + +### 카테고리별 이슈 관리 + +#### 리스트뷰에서 카테고리별 확인 + +리스트뷰에서는 카테고리별로 그룹화된 이슈들을 한눈에 확인할 수 있습니다: + +- **카테고리별 이슈 수**: 각 그룹 헤더에 포함된 이슈 개수 표시 +- **그룹 접힘/펼침**: 필요한 카테고리만 선택적으로 확인 +- **"No Category" 그룹**: 미분류 이슈들의 별도 관리 + +#### 카테고리별 필터링 + +특정 카테고리의 이슈만 확인하고 싶을 때: + +1. **Filter** 버튼을 클릭합니다 +2. **Category** 조건을 추가합니다 +3. 원하는 카테고리를 선택합니다 + +--- + +## 관련 문서 + +- [피드백 관리](./04-feedback-management.md) - 피드백에서 이슈 생성 및 연결 방법 +- [이슈 트래커 연동](/docs/01-user-guide/07-settings/03-issue-tracker-management.md) - 외부 도구와의 연동 설정 +- [프로젝트 관리](./02-project-management.md) - 팀 구성 및 권한 관리 diff --git a/apps/docs/docs/01-user-guide/07-settings/01-tenant-settings.md b/apps/docs/docs/01-user-guide/07-settings/01-tenant-settings.md new file mode 100644 index 000000000..286debb94 --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/01-tenant-settings.md @@ -0,0 +1,235 @@ +--- +title: Tenant 설정 +description: ABC User Feedback의 테넌트 정보, 로그인 방식, 사용자 관리 등 조직 전체에 영향을 주는 설정을 관리하는 방법을 안내합니다. +sidebar_position: 1 +--- + +# Tenant 설정 + +테넌트 설정은 ABC User Feedback의 최상위 관리 기능으로, 조직 전체에 영향을 주는 중요한 설정들을 다룹니다. 이 문서에서는 테넌트 정보 관리, 로그인 방식 설정, 전체 사용자 관리 방법을 설명합니다. + +**주의**: 이 설정들은 **Super Admin 권한**을 가진 사용자만 접근할 수 있습니다. + +--- + +## Tenant 설정 + +테넌트는 조직의 최상위 단위로, 모든 프로젝트와 사용자가 포함되는 범위입니다. + +### 접근 방법 + +1. 우측 상단 메뉴에서 **Home** 아이콘을 클릭합니다 +2. 좌측 메뉴에서 **Tenant Information**을 선택합니다 + +### 수정 가능한 항목 + +| 항목 | 설명 | 수정 가능 여부 | 예시 | +| --------------- | ------------------------------------- | -------------- | ------------------------- | +| **ID** | 테넌트 고유 식별자 (시스템 자동 생성) | ❌ 수정 불가 | `1` | +| **Name** | 테넌트 이름 (조직명, 회사명 등) | ✅ 수정 가능 | `ABC Company` | +| **Description** | 테넌트 설명 (선택사항) | ✅ 수정 가능 | `고객 피드백 관리 시스템` | + +### 정보 수정 방법 + +1. **Name** 또는 **Description** 필드를 수정합니다 +2. 우측 상단의 **Save** 버튼을 클릭합니다 +3. 저장 완료 시 성공 메시지가 표시됩니다 + +> 테넌트 이름은 로그인 UI에 표시될 수 있습니다. + +--- + +## 로그인 설정 + +사용자가 시스템에 접근할 때 사용할 인증 방식을 설정합니다. + +### 접근 방법 + +1. 우측 상단 메뉴에서 **Home** 아이콘을 클릭합니다 +2. 좌측 메뉴에서 **Login Management**를 선택합니다 + +### 지원하는 로그인 방식 + +#### 1. 이메일 로그인 + +기본적으로 제공되는 이메일 + 비밀번호 조합 방식입니다. + +**특징**: + +- 별도 설정 없이 기본 활성화 +- 사용자 초대 → 이메일 인증 → 비밀번호 설정 순서 +- 비밀번호 재설정 기능 제공 + +**비밀번호 정책**: + +- 최소 8자 이상 +- 영문자, 숫자, 특수문자 포함 권장 +- 연속 문자 금지 (예: `aa`, `11`) + +#### 2. Google 로그인 + +Google OAuth 2.0을 통한 소셜 로그인 방식입니다. + +**설정 방법**: + +1. **Google 로그인 활성화**: 토글을 ON으로 전환합니다 +2. **Google Cloud Console 설정이 필요합니다**: + +> **참고**: Google OAuth 연동의 상세한 구현 방법은 [OAuth 연동 가이드](/docs/02-developer-guide/03-oauth-integration.md)를 참조하세요. + +#### 3. 커스텀 OAuth 로그인 + +자체 OAuth 서버나 다른 OAuth 제공자를 사용하는 방식입니다. + +**설정 항목**: + +| 항목 | 설명 | 예시 | +| ----------------- | ----------------------------- | ------------------------------------------ | +| **Provider Name** | 로그인 버튼에 표시될 이름 | `Microsoft로 로그인` | +| **Client ID** | OAuth 클라이언트 ID | `abc123xyz` | +| **Client Secret** | OAuth 클라이언트 시크릿 | `supersecret` | +| **Auth URL** | 인증 요청 URL | `https://auth.example.com/oauth2/auth` | +| **Token URL** | 토큰 요청 URL | `https://auth.example.com/oauth2/token` | +| **User Info URL** | 사용자 정보 요청 URL | `https://auth.example.com/oauth2/userinfo` | +| **Scope** | 요청할 권한 범위 | `openid email profile` | +| **Email Key** | 사용자 정보에서 이메일 필드명 | `email` | + +**설정 순서**: + +1. 각 필드에 OAuth 서버 정보를 입력합니다 +2. **Save** 버튼을 클릭하여 저장합니다 +3. 로그인 화면에서 설정된 Provider Name으로 버튼이 표시됩니다 + +### 로그인 방식 조합 + +여러 로그인 방식을 동시에 활성화할 수 있습니다: + +- **이메일만**: 기본 로그인 폼만 표시 +- **이메일 + Google**: 로그인 폼 + "Google로 로그인" 버튼 +- **이메일 + 커스텀**: 로그인 폼 + 커스텀 OAuth 버튼 + +### 로그인 설정 테스트 + +설정 변경 후 반드시 테스트를 진행하세요: + +1. 브라우저 시크릿 모드로 로그인 페이지 접속 +2. 설정한 로그인 방식들이 정상 표시되는지 확인 +3. 각 방식으로 실제 로그인 테스트 수행 + +--- + +## 사용자 관리 + +테넌트 전체 사용자를 중앙에서 통합 관리하는 기능입니다. + +### 접근 방법 + +1. 우측 상단 메뉴에서 **Home** 아이콘을 클릭합니다 +2. 좌측 메뉴에서 **User Management**를 선택합니다 + +### 사용자 목록 조회 + +#### 표시되는 정보 + +| 컬럼 | 설명 | 표시 예시 | +| ---------- | ------------------------- | ---------------------- | +| Email | 로그인 계정 이메일 | `user@company.com` | +| Name | 사용자 이름 (프로필 기준) | `김사용자` | +| Department | 소속 부서 | `개발팀` | +| Type | 사용자 유형 | `SUPER` / `GENERAL` | +| Project | 접근 가능한 프로젝트 목록 | `프로젝트A, 프로젝트B` | +| Created | 계정 생성일시 | `2024-03-15 14:30` | + +#### 사용자 유형 설명 + +| 유형 | 설명 | 권한 범위 | +| --------- | -------------------------------------------------------- | --------------- | +| `SUPER` | 모든 프로젝트 및 설정 접근 가능. 전체 시스템 관리자 역할 | 테넌트 전체 | +| `GENERAL` | 지정된 프로젝트에만 접근 가능 | 특정 프로젝트만 | + +### 사용자 검색 및 필터링 + +대량의 사용자가 있을 때 원하는 사용자를 빠르게 찾을 수 있습니다. + +#### 필터 기능 + +상단의 **Filter** 버튼을 클릭하여 조건을 설정합니다. + +**필터 조건**: + +- **Email**: 이메일 주소로 검색 +- **Name**: 사용자 이름으로 검색 +- **Department**: 부서명으로 검색 + +**연산자 옵션**: + +- **CONTAINS**: 포함하는 경우 +- **IS**: 정확히 일치하는 경우 + +### 사용자 초대 + +새로운 사용자를 시스템에 초대합니다. + +#### 초대 방법 + +1. 우측 상단의 **Invite User** 버튼을 클릭합니다 +2. 초대 정보를 입력합니다 + +| 항목 | 설명 | 선택 사항 | +| ----------- | --------------------------- | ----------------------------- | +| **Email** | 초대할 사용자의 이메일 주소 | 필수 입력 | +| **Type** | 사용자 유형 | `GENERAL` / `SUPER` | +| **Project** | 접근 허용할 프로젝트 | 프로젝트 목록에서 선택 | +| **Role** | 해당 프로젝트에서의 역할 | `Admin` / `Editor` / `Viewer` | + +3. **Invite** 버튼을 클릭하여 초대를 완료합니다 + +#### 초대 후 프로세스 + +1. 초대된 사용자에게 이메일이 발송됩니다 +2. 사용자가 이메일의 링크를 클릭하여 가입 절차를 진행합니다 +3. 가입 완료 후 지정된 프로젝트에 자동으로 추가됩니다 + +### 사용자 정보 수정 + +기존 사용자의 정보와 권한을 수정할 수 있습니다. + +#### 수정 방법 + +1. 사용자 목록에서 수정하려는 사용자를 클릭합니다 +2. **Edit User** 팝업이 열립니다 + +#### 수정 가능한 항목 + +| 항목 | 수정 가능 여부 | 설명 | +| --------- | -------------- | ------------------------------ | +| **Email** | ❌ 수정 불가 | 계정 식별자로 변경 불가 | +| **Type** | ✅ 수정 가능 | `GENERAL` ↔ `SUPER` 변경 가능 | + +#### 저장 및 적용 + +1. 필요한 정보를 수정합니다 +2. **Save** 버튼을 클릭합니다 +3. 변경사항이 즉시 적용되며, 해당 사용자의 다음 로그인부터 반영됩니다 + +### 사용자 삭제 + +더 이상 시스템을 사용하지 않는 사용자를 삭제할 수 있습니다. + +#### 삭제 방법 + +1. 사용자 수정 팝업에서 하단의 **Delete** 버튼을 클릭합니다 +2. 확인 대화상자에서 삭제를 승인합니다 + +#### 삭제 시 주의사항 + +- **복구 불가능**: 삭제된 사용자 계정은 되돌릴 수 없습니다 +- **접근 권한 즉시 제거**: 삭제 즉시 모든 시스템 접근이 차단됩니다 + +--- + +## 관련 문서 + +- [프로젝트 관리](../02-project-management.md) - 프로젝트별 멤버 및 권한 관리 +- [OAuth 연동 가이드](../../02-developer-guide/03-oauth-integration.md) - OAuth 설정의 기술적 구현 방법 +- [API 연동](/docs/02-developer-guide/02-api-integration.md) - API를 통한 사용자 관리 방법 diff --git a/apps/docs/docs/01-user-guide/07-settings/02-api-key-management.md b/apps/docs/docs/01-user-guide/07-settings/02-api-key-management.md new file mode 100644 index 000000000..ce4751309 --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/02-api-key-management.md @@ -0,0 +1,134 @@ +--- +title: API 키 설정 +description: ABC User Feedback에서 외부 시스템 연동을 위한 API 키를 생성, 관리하고 보안을 유지하는 방법을 설명합니다. +sidebar_position: 2 +--- + +# API 키 설정 + +API 키는 외부 시스템이 ABC User Feedback과 안전하게 연동할 수 있도록 하는 인증 수단입니다. 이 문서에서는 API 키 생성부터 관리, 보안 유지까지 화면 중심으로 설명합니다. + +![api-key-setting.png](/img/api-key/api-key-setting.png) + +--- + +## API 키 개요 + +### API 키의 역할 + +API 키는 다음과 같은 목적으로 사용됩니다: + +- **외부 시스템 인증**: 웹사이트, 모바일 앱에서 피드백 전송 +- **자동화 연동**: 배치 작업, 스크립트를 통한 데이터 수집 +- **서드파티 도구 연결**: 분석 도구, 모니터링 시스템 연동 +- **보안 제어**: 프로젝트별 독립적인 접근 권한 관리 + +### 보안 특징 + +- **프로젝트별 독립성**: 각 프로젝트마다 별도의 키 발급 +- **상태 관리**: Active/Inactive 상태로 즉시 제어 가능 + +--- + +## API 키 생성 + +### 접근 방법 + +1. 상단 메뉴에서 **Settings** 클릭 +2. 좌측 메뉴에서 **API 키 관리** 선택 + +### 키 생성 과정 + +#### 1. 생성 버튼 클릭 + +API 키 관리 화면에서 우측 상단의 **Create API Key** 버튼을 클릭합니다. + +#### 2. 자동 생성 및 표시 + +버튼 클릭 즉시 새로운 API 키가 자동으로 생성되고 팝업으로 표시됩니다. + +**팝업 구성 요소**: + +- **API 키 값**: 전체 키 문자열 표시 +- **Copy 버튼**: 클립보드로 즉시 복사 + +--- + +## API 키 목록 관리 + +### 키 목록 화면 구성 + +생성된 API 키들은 테이블 형태로 관리됩니다. + +#### 테이블 컬럼 정보 + +| 컬럼 | 설명 | 표시 형태 | +| ----------- | ---------------- | -------------------- | +| **API 키** | 생성된 키 값 | `AbcdEfgh...` | +| **Status** | 현재 활성화 상태 | Active / Inactive | +| **Created** | 키 생성 일시 | `2024-03-15 14:30` | +| **Actions** | 관리 액션 버튼들 | 상태 변경, 삭제 버튼 | + +#### 키 식별 방법 + +전체 키 값을 다시 볼 수 없으므로 다음 방법으로 키를 구분합니다: + +- **생성 시간**: 언제 만들어진 키인지 확인 +- **사용 목적 메모**: 별도로 키의 용도를 기록해 두기 + +--- + +## API 키 상태 관리 + +![api-key-detail.png](/img/api-key/api-key-detail.png) + +### Active / Inactive 전환 + +각 API 키는 즉시 활성화/비활성화할 수 있습니다. + +#### 상태별 의미 + +| 상태 | 설명 | API 호출 결과 | +| ------------ | ------------------------- | ---------------- | +| **Active** | 실제 API 호출에 사용 가능 | 정상 처리 | +| **Inactive** | 호출 차단 상태 | 401 Unauthorized | + +#### 상태 변경 방법 + +1. API 키 목록에서 **Status** 컬럼의 토글 스위치를 클릭합니다 +2. 상태가 즉시 변경되며 화면에 반영됩니다 +3. 해당 키를 사용하는 외부 시스템에서 즉시 영향을 받습니다 + +--- + +## API 키 삭제 + +### 삭제 시점 + +다음과 같은 경우 API 키를 삭제해야 합니다: + +- **키 노출**: 실수로 키가 공개된 경우 +- **프로젝트 종료**: 해당 프로젝트 사용 종료 +- **보안 정책**: 정기적 키 교체 정책에 따라 +- **미사용 키**: 더 이상 사용하지 않는 키 정리 + +### 삭제 방법 + +#### 1. 삭제 버튼 클릭 + +키 목록에서 삭제하려는 키의 **Actions** 컬럼에서 삭제 버튼을 클릭합니다. + +#### 2. 삭제 확인 + +확인 대화상자에서 삭제를 최종 승인합니다. + +#### 3. 삭제 완료 + +**Delete** 버튼 클릭 시 키가 즉시 삭제되며 목록에서 제거됩니다. + +--- + +## 관련 문서 + +- [API 연동 가이드](/docs/02-developer-guide/02-api-integration.md) - API 키를 사용한 실제 연동 구현 방법 +- [프로젝트 관리](/docs/01-user-guide/02-project-management.md) - 프로젝트별 API 키 관리 diff --git a/apps/docs/docs/01-user-guide/07-settings/03-issue-tracker-management.md b/apps/docs/docs/01-user-guide/07-settings/03-issue-tracker-management.md new file mode 100644 index 000000000..ea44046bc --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/03-issue-tracker-management.md @@ -0,0 +1,165 @@ +--- +title: 이슈 트래커 설정 +description: ABC User Feedback에서 외부 이슈 트래커(Jira 등)와 연동하여 이슈를 추적하고 링크하는 방법을 안내합니다. +sidebar_position: 3 +--- + +# 이슈 트래커 설정 + +이슈 트래커 설정으로 ABC User Feedback의 이슈를 외부 이슈 관리 시스템(Jira 등)과 연동할 수 있습니다. 내부 이슈에 외부 티켓 링크를 연결하여 개발 워크플로우와 자연스럽게 통합할 수 있습니다. + +--- + +## 이슈 트래커 개요 + +### 연동의 목적 + +이슈 트래커 연동은 다음과 같은 목적으로 사용됩니다: + +- **워크플로우 통합**: 고객 피드백과 개발 작업 연결 +- **이슈 추적**: 내부 이슈와 외부 티켓의 일대일 매핑 +- **진행 상황 공유**: 개발팀과 고객지원팀 간 정보 동기화 +- **효율성 향상**: 중복 작업 방지 및 컨텍스트 유지 + +### 연동 방식 + +- **수동 링크 연결**: ABC 이슈에 외부 티켓 번호를 수동으로 입력 +- **URL 자동 생성**: 설정된 Base URL과 Project Key로 자동 링크 생성 +- **클릭 이동**: 생성된 링크를 클릭하여 외부 시스템으로 바로 이동 + +> **참고**: 실시간 양방향 동기화는 지원하지 않습니다. 상태 변경과 같은 동기화가 필요한 경우 [Webhook](./04-webhook-management.md)을 활용하세요. + +--- + +## 설정 화면 접근 + +### 접근 방법 + +1. 상단 메뉴에서 **Settings** 클릭 +2. 좌측 메뉴에서 **이슈 트래커 관리** 선택 + +### 설정 화면 구성 + +이슈 트래커 관리 화면은 다음과 같이 구성됩니다: + +- **Issue Tracking System**: 연동할 시스템 선택 드롭다운 +- **Connection Settings**: 연결 정보 입력 영역 +- **Link Preview**: 생성될 링크 미리보기 + +--- + +## Jira 연동 설정 + +### 시스템 선택 + +#### 1. Issue Tracking System 설정 + +드롭다운에서 **Jira**를 선택합니다. + +### 연결 정보 입력 + +#### 2. Base URL 설정 + +Jira 시스템의 기본 주소를 입력합니다. + +| 입력 예시 | 설명 | +| ----------------------------------- | ----------------------- | +| `https://yourcompany.atlassian.net` | Jira Cloud 인스턴스 | +| `https://jira.company.com` | 자체 호스팅 Jira Server | +| `https://jira.internal:8080` | 내부 네트워크 Jira | + +**주의사항**: + +- `https://` 또는 `http://` 프로토콜 포함 필수 +- 마지막 슬래시(`/`) 제거 +- 포트 번호가 있는 경우 포함 + +#### 3. Project Key 설정 + +Jira 프로젝트의 고유 키를 입력합니다. + +| 입력 예시 | 설명 | +| --------- | -------------------- | +| `PROJ` | 일반적인 프로젝트 키 | +| `DEV` | 개발팀 프로젝트 | +| `CS` | 고객지원팀 프로젝트 | +| `BUG` | 버그 관리 전용 | + +**Project Key 확인 방법**: + +1. Jira에서 해당 프로젝트 접속 +2. 이슈 번호에서 `-` 앞부분이 Project Key +3. 예: `PROJ-123`에서 `PROJ`가 Project Key + +### 링크 미리보기 + +설정한 정보를 바탕으로 생성될 링크를 미리 확인할 수 있습니다. + +#### 미리보기 구성 + +``` +Base URL + /browse/ + Project Key + - + Issue Number +``` + +**예시**: + +- Base URL: `https://yourcompany.atlassian.net` +- Project Key: `PROJ` +- Issue Number: `123` (사용자가 입력) +- **생성된 링크**: `https://yourcompany.atlassian.net/browse/PROJ-123` + +#### 링크 형식 검증 + +미리보기로 다음 사항을 확인하세요: + +- URL 형식이 올바른지 +- 실제 Jira 이슈 접근 가능한지 +- 팀원들이 접근 권한을 가지고 있는지 + +--- + +## 이슈에서 티켓 연결 + +### 티켓 번호 입력 + +이슈 트래커 설정이 완료되면 개별 이슈에서 외부 티켓을 연결할 수 있습니다. + +#### 연결 방법 + +1. **Issue** 탭에서 원하는 이슈를 클릭합니다 +2. 우측 **Issue Details** 패널이 열립니다 +3. **Ticket** 필드에 외부 티켓 번호를 입력합니다 + +#### 입력 형식 + +| 입력 방식 | 설명 | 생성되는 링크 | +| --------- | ----------- | ------------------------------------------ | +| `123` | 숫자만 입력 | `https://jira.company.com/browse/PROJ-123` | + +시스템이 자동으로 Project Key를 추가하므로 숫자만 입력해도 됩니다. + +### 링크 자동 생성 + +티켓 번호 입력 후 **Save** 버튼을 클릭하면 링크가 자동으로 생성됩니다. + +#### 링크 기능 + +- **클릭 이동**: 링크 클릭 시 새 탭에서 외부 Jira 이슈로 이동 +- **외부 링크 표시**: 링크 옆에 외부 링크 아이콘 표시 +- **수정 가능**: Edit 모드에서 티켓 번호 변경 가능 + +### 연결 해제 + +외부 티켓 연결을 제거하려면: + +1. 이슈 상세 패널에서 **Edit** 버튼 클릭 +2. **Ticket** 필드의 내용을 삭제 +3. **Save** 버튼으로 변경사항 저장 + +--- + +## 관련 문서 + +- [이슈 관리](/docs/01-user-guide/05-issue-management.md) - 이슈 생성 및 티켓 연결 사용법 +- [Webhook 관리](/docs/01-user-guide/07-settings/04-webhook-management.md) - 이슈 상태 변경 시 외부 시스템 알림 +- [API 연동 가이드](/docs/02-developer-guide/02-api-integration.md) - API를 통한 이슈 관리 diff --git a/apps/docs/docs/01-user-guide/07-settings/04-webhook-management.md b/apps/docs/docs/01-user-guide/07-settings/04-webhook-management.md new file mode 100644 index 000000000..bac0ead11 --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/04-webhook-management.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 4 +title: '웹훅 설정' +description: '외부 시스템과 자동 연동을 위해 웹훅을 설정하고 이벤트 발생 시 알림을 전송하는 방법을 설명합니다.' +--- + +# 웹훅 설정 + +웹훅은 ABC User Feedback에서 **특정 이벤트 발생 시 외부 시스템으로 자동 알림**을 전송하는 기능입니다. 피드백 생성, 이슈 상태 변경 등의 이벤트를 실시간으로 외부 서비스(Slack, Discord, 자체 서버 등)에 전달할 수 있습니다. 자세한 연동 가이드는 [웹훅 연동](/docs/02-developer-guide/04-webhook-integration.md) 문서를 참고하세요. + +![webhook-setting](/img/webhook/webhook-setting.png) + +--- + +## 접근 방법 + +1. 상단 메뉴에서 **Settings** 클릭 +2. 좌측 메뉴에서 **Webhook Integration** 선택 + +--- + +## Webhook Integration 화면 개요 + +![webhook-list.png](/img/webhook/webhook-list.png) + +웹훅 연동 화면은 다음처럼 구성됩니다: + +### 웹훅 목록 테이블 구성 + +| 컬럼 | 설명 | +| ----------------- | -------------------------------- | +| **On/Off** | 웹훅 활성화/비활성화 토글 스위치 | +| **Name** | 웹훅 이름 | +| **URL** | 알림을 받을 외부 엔드포인트 | +| **Event Trigger** | 구독 중인 이벤트 트리거 | +| **Created** | 웹훅 생성일시 | + +--- + +## 새 웹훅 생성하기 + +![webhook-create](/img/webhook/webhook-create.png) + +### 1. 웹훅 등록 시작 + +우측 상단의 **Register Webhook** 버튼을 클릭하면 웹훅 등록 모달이 열립니다. + +### 2. 기본 정보 입력 + +#### 필수 입력 항목 + +| 항목 | 설명 | +| -------- | -------------------------------- | +| **Name** | 웹훅 식별을 위한 이름 | +| **URL** | HTTP POST 요청을 받을 엔드포인트 | + +### 3. 토큰 설정 (선택사항) + +**Token** 필드에서 인증을 위한 토큰을 설정할 수 있습니다: + +- **Generate** 버튼을 클릭하여 자동 생성 +- 또는 직접 토큰 값 입력 + +### 4. Event Trigger 선택 + +구독할 이벤트를 채널별로 선택할 수 있습니다: + +#### 지원되는 이벤트 타입 + +각 채널(VOC, Review, Survey, VOC Test)에 대해 다음 이벤트를 선택할 수 있습니다: + +| 이벤트 타입 | 설명 | +| ----------------------- | ----------------------------- | +| **Feedback Creation** | 새 피드백이 등록되었을 때 | +| **Issue Registration** | 새 이슈가 생성되었을 때 | +| **Issue Status Change** | 이슈 상태가 변경되었을 때 | +| **Issue Creation** | 이슈가 피드백에 연결되었을 때 | + +### 5. 웹훅 저장 + +모든 정보를 입력한 후: + +1. **OK** 버튼을 클릭하여 웹훅 생성 +2. **Cancel** 버튼으로 취소 가능 + +--- + +## 웹훅 상태 관리 + +### 활성화/비활성화 전환 + +웹훅 목록에서 각 웹훅의 **On/Off** 컬럼에 있는 토글 스위치를 클릭하여 상태를 변경할 수 있습니다: + +- **On (활성화)**: 이벤트 발생 시 실시간 전송 +- **Off (비활성화)**: 웹훅은 유지되지만 전송 중단 + +### 임시 비활성화 시나리오 + +- 외부 서버 점검 중일 때 +- 웹훅 URL 변경 작업 중일 때 +- 스팸성 알림 방지가 필요할 때 + +--- + +## 웹훅 편집 및 삭제 + +### 웹훅 편집 + +웹훅 목록에서 수정하려는 웹훅을 클릭하면 편집 모달이 열립니다: + +#### 편집 가능한 항목 + +- 웹훅 이름 +- Target URL 변경 +- 토큰 값 수정 +- 이벤트 타입 추가/제거 + +### 웹훅 삭제 + +웹훅을 완전히 제거하려면: + +1. 편집 모달에서 삭제 옵션 선택 +2. 또는 목록에서 직접 삭제 버튼 클릭 (UI에 삭제 버튼이 있는 경우) + +--- + +## 웹훅 테스트 및 검증 + +### 수동 검증 방법 + +1. **피드백 생성 테스트**: + - 테스트 피드백을 등록하여 `Feedback Creation` 이벤트 확인 +2. **이슈 관리 테스트**: + - 이슈를 생성하거나 상태를 변경하여 관련 이벤트 확인 +3. **외부 서비스 확인**: + - Slack, Discord 등에서 메시지 수신 여부 확인 + +--- + +## 일반적인 연동 예시 + +### Slack 웹훅 설정 + +``` +Name: Slack 알림 +URL: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX +Events: Feedback Creation, Issue Registration (모든 채널) +``` + +### Discord 웹훅 설정 + +``` +Name: Discord 개발팀 알림 +URL: https://discord.com/api/webhooks/123456789/abcdefghijk +Events: Issue Status Change (VOC 채널만) +``` + +### 커스텀 서버 연동 + +``` +Name: 내부 분석 시스템 +URL: https://api.yourcompany.com/webhooks/feedback +Token: your-generated-token +Events: 모든 이벤트 (전체 채널) +``` + +--- + +## 관련 문서 + +- [Webhook 개발자 가이드](/docs/02-developer-guide/04-webhook-integration.md) - 웹훅 수신 서버 구현 방법 +- [API 키 관리](./02-api-key-management.md) - API 키 기반 인증 설정 diff --git a/apps/docs/docs/01-user-guide/07-settings/05-ai-setting.md b/apps/docs/docs/01-user-guide/07-settings/05-ai-setting.md new file mode 100644 index 000000000..c69bdf456 --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/05-ai-setting.md @@ -0,0 +1,272 @@ +--- +sidebar_position: 5 +title: 'AI 설정' +description: '생성형 AI 기능을 사용하기 위한 기본 설정 및 연동 방법을 설명합니다.' +--- + +# AI 설정 + +ABC User Feedback에서 **생성형 AI 기능**을 사용하려면 먼저 AI 제공자와의 연동을 설정해야 합니다. +AI 설정을 완료하면 **AI 필드 템플릿**, **AI 이슈 추천**, **AI 사용량 모니터링** 등의 모든 기능을 활용할 수 있습니다. + +--- + +## 접근 방법 + +1. 상단 메뉴에서 **Settings** 클릭 +2. 좌측 메뉴에서 **Generative AI Integration** 선택 +3. 상단 탭에서 **AI Setting** 클릭 + +--- + +## AI 제공자 선택 및 설정 + +![ai-setting.png](/img/ai/ai-setting.png) + +### 1. 제공자 선택 + +현재 지원되는 AI 제공자 OpenAI, Google Gemini 중 하나를 선택합니다: + +### 2. API 키 입력 + +선택한 AI 제공자에서 발급받은 API 키를 입력합니다. + +### 3. Base URL 설정 (선택사항) + +대부분의 경우 **비워두면 기본값**이 자동으로 사용됩니다. + +특별한 엔드포인트나 프록시 서버를 사용하는 경우에만 입력하세요. + +### 4. System Prompt 설정 (선택사항) + +AI가 모든 요청을 처리할 때 참고할 **기본 지시사항**을 설정할 수 있습니다. + +조직의 톤앤매너나 특별한 요구사항이 있을 때 활용하세요. + +### 5. 설정 저장 + +모든 정보를 입력한 후 우측 상단의 **Save** 버튼을 클릭합니다. + +--- + +## AI 사용량 모니터링 + +![ai-usage.png](/img/ai/ai-usage.png) + +**AI Usage** 탭에서 AI 기능 사용량과 비용을 모니터링할 수 있습니다. + +### 사용량 대시보드 + +확인할 수 있는 정보: + +- **일별/월별 API 호출 수** +- **토큰 사용량** (입력/출력 별도) +- **기능별 사용 분포** (AI 필드 vs 이슈 추천) + +--- + +## AI 필드 템플릿 관리 + +![ai-field-template.png](/img/ai/ai-field-template.png) + +AI 설정을 완료한 후, **AI Field Template** 탭에서 피드백 자동 분석 템플릿을 관리할 수 있습니다. + +### 기본 제공 템플릿 + +시스템에서 제공하는 기본 템플릿들: + +| 템플릿 | 설명 | 활용 예시 | +| ---------------------- | -------------------------- | ----------------------- | +| **Feedback Summary** | 피드백을 1문장으로 요약 | 긴 피드백의 핵심 파악 | +| **Sentiment Analysis** | 감정 분석 (긍정/부정/중립) | 고객 만족도 추세 분석 | +| **Translation** | 피드백을 영어로 번역 | 다국어 피드백 통합 분석 | +| **Keyword Extraction** | 핵심 키워드 2-3개 추출 | 이슈 카테고리 자동 태깅 | + +### 커스텀 템플릿 생성 + +![ai-field-template-create.png](/img/ai/ai-field-template-create.png) + +1. **Create New** 카드 클릭 +2. 템플릿 정보 입력 + +| 항목 | 설명 | +| --------------- | --------------------- | +| **Title** | 템플릿 이름 | +| **Prompt** | AI에게 줄 지시 문장 | +| **Model** | 사용할 AI 모델 선택 | +| **Temperature** | 창의성 조절 (0.0~1.0) | + +3. Playground에서 테스트 + +- "Add Data" 버튼으로 테스트 피드백 입력 +- "AI test execution" 클릭으로 결과 확인 + +### 템플릿 편집 및 삭제 + +- 템플릿 카드 클릭 → 편집 +- **Delete Template** 버튼으로 삭제 +- 삭제 시 해당 템플릿을 사용하는 AI 필드에 영향을 줄 수 있음 + +--- + +## AI 필드를 채널에 적용하기 + +AI 필드 템플릿을 생성한 후, 실제 채널의 필드로 적용해야 피드백에서 AI 분석 결과를 확인할 수 있습니다. + +### 1. Field Management에서 AI 필드 추가 + +**Settings > Channel List > [채널 선택] > Field Management**에서 AI 필드를 추가합니다. + +#### AI 필드 설정 항목 + +| 항목 | 설명 | 필수 여부 | +| ----------------------- | -------------------------- | --------- | +| **Key** | 필드 고유 식별자 | 필수 | +| **Display Name** | UI에 표시될 이름 | 필수 | +| **Format** | `aiField` 선택 | 필수 | +| **Template** | 생성한 AI 필드 템플릿 선택 | 필수 | +| **Target Field** | 분석 대상이 될 텍스트 필드 | 필수 | +| **Property** | Editable 또는 Read Only | 필수 | +| **AI Field Automation** | 자동 실행 여부 | 선택 | + +#### 설정 예시 + +``` +Key: sentiment_analysis +Display Name: 감정 분석 +Format: aiField +Template: Feedback Sentiment Analysis +Target Field: message +Property: Read Only +AI Field Automation: ON (자동 실행) +``` + +### 2. Template 연결 및 Target Field 설정 + +**Template** 드롭다운에서 이전에 생성한 AI 필드 템플릿을 선택합니다. + +**Target Field**는 AI 분석의 대상이 될 필드들을 지정합니다 + +### 3. AI Field Automation 설정 + +**AI Field Automation** 토글을 통해 실행 방식을 선택합니다: + +- **ON (자동 실행)**: 새 피드백 등록 시 자동으로 AI 분석 실행 +- **OFF (수동 실행)**: 사용자가 수동으로 실행 버튼을 클릭해야 함 + +## 피드백에서 AI 분석 결과 확인 + +AI 필드 설정이 완료되면 피드백 목록과 상세 화면에서 AI 분석 결과를 확인할 수 있습니다. + +### 피드백 목록에서 확인 + +피드백 테이블에 AI 필드가 새로운 컬럼으로 추가됩니다: + +- **Summary**: AI가 생성한 요약 +- **Classification**: AI 분류 결과 +- **Korean**: 번역 결과 등 + +### 피드백 상세 화면에서 확인 + +피드백 상세 보기 패널에서 더 자세한 AI 분석 결과를 확인할 수 있습니다: + +1. 피드백 행 클릭 → 우측 상세 패널 열림 +2. AI 필드별 분석 결과 확인 +3. 각 AI 필드마다 분석 결과와 함께 표시 + +## AI 분석 수동 실행 + +피드백 상세 화면에서 수동으로 AI 분석을 실행할 수 있습니다. + +### Run AI 버튼 사용 + +1. 피드백 상세 화면에서 **Run AI** 버튼 클릭 +2. AI 분석이 실행되고 결과가 해당 필드에 자동 입력됨 +3. 분석 완료 후 결과 즉시 확인 가능 + +### 수동 실행 활용 시나리오 + +- **비용 절약**: 필요한 피드백만 선별하여 AI 분석 +- **성능 확인**: 새로운 템플릿의 결과를 미리 테스트 +- **재분석**: 템플릿 수정 후 기존 피드백 재분석 + +--- + +## AI 이슈 추천 설정 + +![ai-issue-recommendation.png](/img/ai/ai-issue-recommendation.png) + +**AI Issue Recommendation** 탭에서 피드백 기반 자동 이슈 추천 기능을 설정할 수 있습니다. + +### AI 이슈 추천 설정 생성 + +![ai-issue-recommendation-create.png](/img/ai/ai-issue-recommendation-create.png) + +1. **Create New** 버튼 클릭 +2. 설정 항목 입력 + +| 항목 | 설명 | 필수 여부 | +| ---------------- | ---------------------------- | --------- | +| **Channel** | 적용할 채널 선택 | 필수 | +| **Target Field** | 분석 대상 필드 (예: message) | 필수 | +| **Prompt** | 추천 기준 프롬프트 | 선택 | +| **Enable** | 기능 활성화 토글 | 필수 | + +3. 고급 설정 + +| 설정 | 설명 | +| ------------------------- | ------------------------------------- | +| **Model** | 사용 모델 | +| **Temperature** | 창의성 조절 | +| **Data Reference Amount** | 참조할 이슈 양 (이슈와 관련 피드백들) | + +### AI 이슈 추천 기능 테스트 + +입력된 설정에 대해 Playground에서 테스트: + +1. 예시 피드백 입력 +2. "AI test execution" 클릭 +3. 추천 이슈 목록 확인 + +### 실제 피드백에서 추천 활용 + +피드백 상세 보기에서: + +- AI 추천 이슈 목록 확인 +- 체크박스로 적절한 이슈 선택 +- **Retry** 버튼으로 다른 추천 요청 + +### 피드백 목록에서 이슈 추천 사용 + +AI 이슈 추천을 설정한 채널에서는 피드백 목록 화면에서도 직접 이슈 추천 기능을 사용할 수 있습니다. + +#### 사용 방법 + +1. 피드백 목록에서 이슈를 연결하고 싶은 피드백의 **Issue 컬럼**에 있는 **+ 버튼** 클릭 +2. 드롭다운 메뉴가 표시되면 **"Run AI"** 선택 +3. AI가 관련 이슈를 분석하여 추천 목록 표시 + +#### 추천 결과 확인 및 적용 + +AI 분석 완료 후 추천 이슈 목록에서: + +- **추천된 이슈들** 확인 +- 추천된 적절한 이슈 선택 +- 새 이슈 생성 옵션도 제공 +- 선택 완료 후 해당 이슈가 피드백에 자동 연결 + +#### 일괄 처리 활용 + +여러 피드백을 선택한 상태에서도 AI 이슈 추천을 사용할 수 있어 효율적인 피드백 분류가 가능합니다: + +1. 피드백 목록에서 여러 행 선택 (체크박스 활용) +2. 상단 일괄 작업 메뉴에서 AI 이슈 추천 실행 +3. 각 피드백별로 추천 이슈 확인 및 적용 + +--- + +## 관련 문서 + +- [필드 설정하기](/docs/01-user-guide/04-feedback-management.md) - AI 필드를 채널에 적용하는 방법 +- [이슈 생성 및 상태 관리](/docs/01-user-guide/05-issue-management.md) - AI 추천 이슈 활용법 +- [피드백 확인 및 필터링](/docs/01-user-guide/04-feedback-management.md) - AI 분석 결과 확인 방법 diff --git a/apps/docs/docs/01-user-guide/07-settings/06-image-setting.md b/apps/docs/docs/01-user-guide/07-settings/06-image-setting.md new file mode 100644 index 000000000..96ebbcf46 --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/06-image-setting.md @@ -0,0 +1,102 @@ +--- +sidebar_position: 6 +title: '이미지 설정' +description: '피드백에 첨부된 이미지 저장 방식과 보안 정책을 설정하는 방법을 안내합니다.' +--- + +# 이미지 설정 + +ABC User Feedback에서는 사용자가 피드백을 제출할 때 **이미지와 함께 업로드**할 수 있도록 지원합니다. 이미지 저장 방식과 보안 정책을 적절히 설정하여 안전하고 효율적인 피드백 수집 환경을 구축할 수 있습니다. + +![image-setting.png](/img/image/image-setting.png) + +--- + +## 접근 방법 + +1. 상단 메뉴에서 **Settings** 클릭 +2. 좌측 메뉴에서 **Channel List > [채널 선택]** +3. 하단 탭 중 **Image Management** 선택 + +--- + +## Image Storage Integration 설정 + +**Multipart Upload API** 방식을 통해 이미지를 서버로 직접 업로드하거나 **Presigned URL Download** 기능을 활용하려면 S3 또는 S3 호환 저장소 연동이 필수입니다. + +### 필수 설정 항목 + +| 항목 | 설명 | 예시 | +| --------------------- | ------------------------- | ----------------------------------------- | +| **Access Key ID** | S3 접근을 위한 키 ID | `AKIAIOSFODNN7EXAMPLE` | +| **Secret Access Key** | 키에 대한 시크릿 | `wJalrXUtnFEMI/K7MDENG/...` | +| **End Point** | S3 API 엔드포인트 URL | `https://s3.ap-northeast-1.amazonaws.com` | +| **Region** | 버킷이 위치한 지역 | `ap-northeast-1` | +| **Bucket Name** | 이미지가 저장될 대상 버킷 | `consumer-ufb-images` | + +### Presigned URL Download 설정 + +**Presigned URL Download** 옵션을 통해 이미지 다운로드 보안을 강화할 수 있습니다. + +#### 설정 옵션 + +- **Enable**: 인증된 일회용 URL을 통해 이미지 접근 (보안 강화) +- **Disable**: 이미지 URL이 직접 노출되어 공개 접근 가능 + +### 연결 테스트 + +모든 설정을 입력한 후 **Test Connection** 버튼을 클릭하여 저장소 연결을 확인합니다. + +연결 결과: + +- ✅ **성공**: "Connection test succeeded" 메시지 +- ❌ **실패**: 입력 값, 버킷 권한, 네트워크 설정 재확인 필요 + +--- + +## Image URL Domain Whitelist 설정 + +**Image URL 방식**을 사용하거나 보안을 강화하고 싶은 경우, 신뢰할 수 있는 도메인만 허용하도록 화이트리스트를 설정할 수 있습니다. + +### 현재 상태 확인 + +기본 설정은 **"All image URLs are allowed"** 상태로, 모든 도메인의 이미지 URL을 허용합니다. + +### 화이트리스트 추가 + +보안 강화를 위해 특정 도메인만 허용하려면: + +1. **Whitelist** 영역에 신뢰할 수 있는 도메인 추가 +2. 예시 도메인: + - `cdn.yourcompany.com` + - `images.trusted-partner.io` + - `storage.googleapis.com` + +--- + +## 지원되는 저장소 서비스 + +### AWS S3 + +- 가장 일반적으로 사용되는 클라우드 스토리지 +- 안정적이고 확장성이 뛰어남 + +--- + +## 설정 저장 + +모든 설정을 완료한 후 우측 상단의 **Save** 버튼을 클릭하여 변경사항을 저장합니다. + +저장 후에는: + +- 새로운 이미지 업로드가 설정한 방식으로 동작 +- 기존 이미지는 기존 설정 그대로 유지 +- Test Connection으로 설정 정상 작동 여부 재확인 권장 + +--- + +## 관련 문서 + +- [필드 설정하기](/docs/01-user-guide/04-feedback-management.md) - 이미지 필드를 피드백 폼에 추가하는 방법 +- [피드백 확인 및 필터링](/docs/01-user-guide/04-feedback-management.md) - 업로드된 이미지를 피드백에서 확인하는 방법 +- [API 키 관리](./02-api-key-management.md) - API 키 보안 관리 방법 diff --git a/apps/docs/docs/01-user-guide/07-settings/_category_.json b/apps/docs/docs/01-user-guide/07-settings/_category_.json new file mode 100644 index 000000000..4e4627726 --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 7, + "label": "설정", + "description": "설정에 대한 가이드입니다." +} diff --git a/apps/docs/docs/01-user-guide/07-settings/index.md b/apps/docs/docs/01-user-guide/07-settings/index.md new file mode 100644 index 000000000..57dd48b09 --- /dev/null +++ b/apps/docs/docs/01-user-guide/07-settings/index.md @@ -0,0 +1,7 @@ +--- +title: 설정 +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/docs/01-user-guide/_category_.json b/apps/docs/docs/01-user-guide/_category_.json new file mode 100644 index 000000000..89ee060a0 --- /dev/null +++ b/apps/docs/docs/01-user-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 2, + "label": "사용자 가이드" +} diff --git a/apps/docs/docs/01-user-guide/index.md b/apps/docs/docs/01-user-guide/index.md new file mode 100644 index 000000000..a56a35457 --- /dev/null +++ b/apps/docs/docs/01-user-guide/index.md @@ -0,0 +1,7 @@ +--- +title: 사용자 가이드 +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/docs/02-developer-guide/01-installation/01-docker-hub-images.md b/apps/docs/docs/02-developer-guide/01-installation/01-docker-hub-images.md new file mode 100644 index 000000000..cb6937322 --- /dev/null +++ b/apps/docs/docs/02-developer-guide/01-installation/01-docker-hub-images.md @@ -0,0 +1,379 @@ +--- +id: docker-hub-images +title: Docker Hub 이미지 설치 +description: Docker Hub에 등록된 ABC User Feedback 공식 이미지로 빠르게 시스템을 설치하는 방법을 설명합니다. +sidebar_position: 1 +--- + +# Docker Hub 이미지 설치 + +ABC User Feedback은 공식 Docker 이미지를 제공합니다. +이 문서는 Docker Compose를 이용하여 **웹 UI, API 서버, 데이터베이스, SMTP 서버** 등 시스템을 로컬에서 빠르게 구성하는 방법을 설명합니다. + +--- + +## 1. 사전 요구 사항 + +| 항목 | 설명 | +| -------------- | ----------------------------------------------------------------- | +| Docker | 20.10 이상 | +| Docker Compose | v2 이상 권장 | +| 사용 포트 | `3000`, `4000`, `13306`, `5080`, `25` (로컬에서 비워져 있어야 함) | + +--- + +## 2. Docker 이미지 구성 + +| 서비스 이름 | 설명 | Docker 이미지 이름 | +| ----------------- | ---------------------------------- | ------------------------------------- | +| Web (Admin UI) | 프론트엔드 웹 UI (Next.js) | `line/abc-user-feedback-web` | +| API (Backend) | 백엔드 서버 (NestJS) | `line/abc-user-feedback-api` | +| MySQL | 데이터베이스 | `mysql:8.0` | +| SMTP4Dev | 로컬 테스트용 이메일 서버 | `rnwood/smtp4dev:v3` | +| (선택) OpenSearch | 검색 기능 및 AI 분석 정확도 향상용 | `opensearchproject/opensearch:2.16.0` | + +--- + +## 3. `docker-compose.yml` 예시 + +```yaml +name: abc-user-feedback +services: + web: + image: line/abc-user-feedback-web:latest + environment: + - NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 + ports: + - 3000:3000 + depends_on: + - api + restart: unless-stopped + + api: + image: line/abc-user-feedback-api:latest + environment: + - JWT_SECRET=jwtsecretjwtsecretjwtsecret + - MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@mysql:3306/userfeedback + - SMTP_HOST=smtp4dev + - SMTP_PORT=25 + - SMTP_SENDER=user@feedback.com + # OpenSearch 사용 시 아래 주석을 제거하세요 + # - OPENSEARCH_USE=true + # - OPENSEARCH_NODE=http://opensearch-node:9200 + ports: + - 4000:4000 + depends_on: + - mysql + restart: unless-stopped + + mysql: + image: mysql:8.0 + command: + [ + "--default-authentication-plugin=mysql_native_password", + "--collation-server=utf8mb4_bin", + ] + environment: + MYSQL_ROOT_PASSWORD: userfeedback + MYSQL_DATABASE: userfeedback + MYSQL_USER: userfeedback + MYSQL_PASSWORD: userfeedback + TZ: UTC + ports: + - 13306:3306 + volumes: + - mysql:/var/lib/mysql + restart: unless-stopped + + smtp4dev: + image: rnwood/smtp4dev:v3 + ports: + - 5080:80 + - 25:25 + - 143:143 + volumes: + - smtp4dev:/smtp4dev + restart: unless-stopped + + # OpenSearch를 사용하려면 아래 주석을 제거하세요 + # opensearch-node: + # image: opensearchproject/opensearch:2.16.0 + # restart: unless-stopped + # environment: + # - cluster.name=opensearch-cluster + # - node.name=opensearch-node + # - discovery.type=single-node + # - bootstrap.memory_lock=true + # - 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m' + # - plugins.security.disabled=true + # - OPENSEARCH_INITIAL_ADMIN_PASSWORD=UserFeedback123!@# + # ulimits: + # memlock: + # soft: -1 + # hard: -1 + # nofile: + # soft: 65536 + # hard: 65536 + # volumes: + # - opensearch:/usr/share/opensearch/data + # ports: + # - 9200:9200 + # - 9600:9600 + +volumes: + mysql: + smtp4dev: + # opensearch: +``` + +--- + +## 4. 실행 단계 + +### 4.1 Docker 이미지 다운로드 및 실행 + +```bash +# Docker Compose로 모든 서비스를 백그라운드에서 실행 +docker compose up -d +``` + +### 4.2 실행 상태 확인 + +```bash +# 모든 컨테이너가 정상적으로 실행 중인지 확인 +docker compose ps +``` + +### 4.3 서비스 접속 확인 + +- **웹 애플리케이션**: [http://localhost:3000](http://localhost:3000) +- **API 서버**: [http://localhost:4000](http://localhost:4000) +- **SMTP 테스트 페이지**: [http://localhost:5080](http://localhost:5080) +- **MySQL 데이터베이스**: `localhost:13306` (사용자: `userfeedback`, 비밀번호: `userfeedback`) + +--- + +## 5. SMTP 설정 + +기본적으로 이 구성에서는 `smtp4dev`를 통해 메일을 테스트할 수 있습니다. + +- **웹 인터페이스**: [http://localhost:5080](http://localhost:5080) +- **SMTP 포트**: `25` +- **IMAP 포트**: `143` + +### SMTP 테스트 방법 + +1. 웹 애플리케이션에서 사용자 가입을 하거나 사용자 초대 기능을 사용 +2. [http://localhost:5080](http://localhost:5080)에서 전송된 이메일 확인 +3. 이메일 내용과 첨부파일 등을 테스트 + +> **중요**: 실제 운영 환경에서는 반드시 외부 SMTP 서버(예: Gmail, SendGrid, 사내 SMTP 등)와 연동해야 합니다. + +👉 운영용 SMTP 연동이 필요하다면 [SMTP 서버 연동 가이드](./04-smtp-configuration.md)를 참고하세요. + +## 6. 설치 확인 + +### 6.1 웹 애플리케이션 접속 확인 + +브라우저에서 `http://localhost:3000`으로 접속하여 다음을 확인하세요: + +- 테넌트 생성 화면이 정상적으로 표시되는지 +- 페이지 로딩이 완료되는지 +- JavaScript 오류가 없는지 (브라우저 개발자 도구에서 확인) + +### 6.2 API 서버 상태 확인 + +```bash +# API 서버 헬스 체크 +curl http://localhost:4000/api/health +``` + +예상 응답: + +```json +{ + "status": "ok", + "info": { + "database": { + "status": "up" + } + } +} +``` + +### 6.3 데이터베이스 연결 확인 + +```bash +# MySQL 컨테이너에 직접 접속하여 데이터베이스 확인 +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "SHOW DATABASES;" + +# 테이블 생성 확인 +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "USE userfeedback; SHOW TABLES;" +``` + +### 6.4 로그 확인 + +```bash +# 모든 서비스의 로그 확인 +docker compose logs + +# 특정 서비스의 로그만 확인 +docker compose logs api +docker compose logs web +docker compose logs mysql +``` + +--- + +## 7. OpenSearch 사용 시 주의사항 + +OpenSearch는 검색 기능과 AI 분석의 정확도를 향상시키는 선택적 구성 요소입니다. + +### 7.1 OpenSearch 활성화 방법 + +1. `docker-compose.yml` 파일에서 `api` 서비스의 환경 변수 주석 해제: + +```yaml +- OPENSEARCH_USE=true +- OPENSEARCH_NODE=http://opensearch-node:9200 +``` + +2. `opensearch-node` 서비스 주석 해제 +3. `volumes:` 섹션에서 `opensearch:` 주석 해제 +4. 포트 `9200`, `9600`이 로컬에서 사용 중이지 않은지 확인 + +### 7.2 메모리 요구사항 + +> **주의**: OpenSearch는 최소 2GB 이상의 메모리를 요구합니다. 메모리 부족 시 컨테이너가 자동 종료될 수 있습니다. + +### 7.3 OpenSearch 상태 확인 + +```bash +# OpenSearch 클러스터 상태 확인 +curl http://localhost:9200/_cluster/health + +# OpenSearch 노드 정보 확인 +curl http://localhost:9200/_nodes + +# 인덱스 확인 +curl http://localhost:9200/_cat/indices +``` + +### 7.4 OpenSearch 비활성화 + +OpenSearch를 사용하지 않으려면 `docker-compose.yml`에서 해당 서비스와 환경 변수를 주석 처리하면 됩니다. + +--- + +## 8. 문제 해결 + +### 8.1 포트 충돌 문제 + +**증상**: `docker compose up` 실행 시 포트 바인딩 오류 발생 + +**해결 방법**: + +```bash +# 사용 중인 포트 확인 +lsof -i :3000 # 웹 포트 +lsof -i :4000 # API 포트 +lsof -i :13306 # MySQL 포트 +lsof -i :5080 # SMTP 포트 + +# 해당 포트를 사용하는 프로세스 종료 후 재시작 +docker compose down +docker compose up -d +``` + +### 8.2 컨테이너 시작 실패 + +**증상**: 일부 컨테이너가 시작되지 않거나 계속 재시작됨 + +**해결 방법**: + +```bash +# 컨테이너 상태 확인 +docker compose ps + +# 실패한 컨테이너의 로그 확인 +docker compose logs [서비스명] + +# 모든 컨테이너 중지 및 제거 +docker compose down + +# 볼륨까지 제거 (데이터 손실 주의) +docker compose down -v + +# 다시 시작 +docker compose up -d +``` + +### 8.3 데이터베이스 연결 오류 + +**증상**: API 서버에서 MySQL 연결 실패 + +**해결 방법**: + +```bash +# MySQL 컨테이너가 완전히 시작될 때까지 대기 +docker compose logs mysql + +# MySQL 컨테이너에 직접 접속 테스트 +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "SELECT 1;" + +# API 서비스 재시작 +docker compose restart api +``` + +### 8.4 이미지 다운로드 실패 + +**증상**: Docker 이미지를 다운로드할 수 없음 + +**해결 방법**: + +```bash +# Docker Hub 로그인 확인 +docker login + +# 이미지 수동 다운로드 +docker pull line/abc-user-feedback-web:latest +docker pull line/abc-user-feedback-api:latest + +# 네트워크 연결 확인 +ping hub.docker.com +``` + +### 8.5 메모리 부족 문제 + +**증상**: OpenSearch 컨테이너가 자동 종료됨 + +**해결 방법**: + +```bash +# 시스템 메모리 확인 +free -h + +# Docker 메모리 사용량 확인 +docker stats + +# OpenSearch 비활성화 (docker-compose.yml에서 주석 처리) +# 또는 메모리 할당량 증가 +``` + +--- + +## 9. 참고 링크 + +- [ABC User Feedback Web - Docker Hub](https://hub.docker.com/r/line/abc-user-feedback-web) +- [ABC User Feedback API - Docker Hub](https://hub.docker.com/r/line/abc-user-feedback-api) +- [smtp4dev - Docker Hub](https://hub.docker.com/r/rnwood/smtp4dev) +- [OpenSearch - Docker Hub](https://hub.docker.com/r/opensearchproject/opensearch) + +--- + +## 관련 문서 + +- [수동 설치 가이드](./03-manual-setup.md) +- [SMTP 서버 연동 가이드](./04-smtp-configuration.md) +- [환경 변수 목록 및 설정](./05-configuration.md) +- [초기 셋팅 가이드](/docs/01-user-guide/01-getting-started.md) diff --git a/apps/docs/docs/02-developer-guide/01-installation/02-cli-tool.md b/apps/docs/docs/02-developer-guide/01-installation/02-cli-tool.md new file mode 100644 index 000000000..49f143b9b --- /dev/null +++ b/apps/docs/docs/02-developer-guide/01-installation/02-cli-tool.md @@ -0,0 +1,272 @@ +--- +sidebar_position: 2 +title: "CLI 도구 사용법" +description: "ABC User Feedback CLI 도구로 빠르고 쉽게 시스템을 설치하고 관리하는 방법을 설명합니다." +--- + +# CLI 도구 사용법 + +ABC User Feedback CLI (`auf-cli`)는 시스템 설치, 실행, 관리를 간소화하는 명령줄 도구입니다. Node.js와 Docker만 설치되어 있으면 추가 의존성 설치나 저장소 클론 없이 `npx`를 통해 바로 실행할 수 있습니다. + +## 주요 기능 + +- 필요한 인프라 자동 설정 (MySQL, SMTP, OpenSearch) +- 환경 변수 설정 간소화 +- API 및 웹 서버 자동 시작/중지 +- 볼륨 데이터 정리 +- 동적 Docker Compose 파일 생성 + +## 사용되는 Docker 이미지 + +- `line/abc-user-feedback-web:latest` - 웹 프론트엔드 +- `line/abc-user-feedback-api:latest` - API 백엔드 +- `mysql:8.0` - 데이터베이스 +- `rnwood/smtp4dev:v3` - SMTP 테스트 서버 +- `opensearchproject/opensearch:2.16.0` - 검색 엔진 (선택사항) + +## 사전 요구사항 + +CLI 도구를 사용하기 전에 다음 요구사항을 충족해야 합니다: + +- [Node.js v22 이상](https://nodejs.org/en/download/) +- [Docker](https://docs.docker.com/desktop/) + +## 기본 명령어 + +### 초기화 + +ABC User Feedback에 필요한 인프라를 설정하려면 다음 명령어를 실행하세요: + +```bash +npx auf-cli init +``` + +이 명령어는 다음 작업을 수행합니다: + +1. 환경 변수 설정을 위한 `config.toml` 파일 생성 +2. 아키텍처(ARM/AMD)에 따라 필요한 인프라 설정 + +초기화가 완료되면 현재 디렉토리에 `config.toml` 파일이 생성됩니다. 필요에 따라 이 파일을 편집하여 환경 변수를 조정할 수 있습니다. + +### 서버 시작 + +API 및 웹 서버를 시작하려면 다음 명령어를 실행하세요: + +```bash +npx auf-cli start +``` + +이 명령어는 다음 작업을 수행합니다: + +1. `config.toml` 파일에서 환경 변수 읽기 +2. Docker Compose 파일 생성 및 서비스 시작 +3. API 및 웹 서버 컨테이너와 필요한 인프라(MySQL, SMTP, OpenSearch) 시작 + +서버가 성공적으로 시작되면 웹 브라우저에서 `http://localhost:3000` (또는 설정된 URL)로 ABC User Feedback 웹 인터페이스에 접근할 수 있습니다. CLI는 다음 URL들을 표시합니다: + +- 웹 인터페이스 URL +- API URL +- MySQL 연결 문자열 +- OpenSearch URL (활성화된 경우) +- SMTP 웹 인터페이스 (smtp4dev 사용 시) + +### 서버 중지 + +API 및 웹 서버를 중지하려면 다음 명령어를 실행하세요: + +```bash +npx auf-cli stop +``` + +이 명령어는 실행 중인 API 및 웹 서버 컨테이너와 인프라 컨테이너를 중지합니다. 볼륨에 저장된 모든 데이터는 보존됩니다. + +### 볼륨 정리 + +시작 중 생성된 Docker 볼륨을 정리하려면 다음 명령어를 실행하세요: + +```bash +npx auf-cli clean +``` + +이 명령어는 모든 컨테이너를 중지하고 MySQL, SMTP, OpenSearch 등의 Docker 볼륨을 삭제합니다. + +**경고**: 이 작업은 모든 데이터를 삭제하므로 백업이 필요한 경우 미리 백업하세요. + +`--images` 옵션을 사용하여 사용하지 않는 Docker 이미지도 정리할 수 있습니다: + +```bash +npx auf-cli clean --images +``` + +## 설정 파일 (config.toml) + +`init` 명령어를 실행하면 현재 디렉토리에 `config.toml` 파일이 생성됩니다. 이 파일은 ABC User Feedback의 환경 변수를 설정하는 데 사용됩니다. + +다음은 `config.toml` 파일의 예시입니다: + +```toml +[web] +port = 3000 +# api_base_url = "http://localhost:4000" + +[api] +port = 4000 +jwt_secret = "jwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecret" + +# master_api_key = "MASTER_KEY" +# access_token_expired_time = "10m" +# refresh_token_expired_time = "1h" + +# [api.auto_feedback_deletion] +# enabled = true +# period_days = 365 + +# [api.smtp] +# host = "smtp4dev" # SMTP_HOST +# port = 25 # SMTP_PORT +# sender = "user@feedback.com" +# username= +# password= +# tls= +# cipher_spec= +# opportunitic_tls= + +# [api.opensearch] +# enabled = true + +[mysql] +port = 13306 +``` + +필요에 따라 이 파일을 편집하여 환경 변수를 조정할 수 있습니다. 환경 변수 자세한 정보는 [환경 변수 설정](./05-configuration.md) 문서를 참조하세요. + +## 고급 사용법 + +### 포트 변경 + +기본적으로 웹 서버는 포트 3000을, API 서버는 포트 4000을 사용합니다. 이를 변경하려면 `config.toml` 파일에서 다음 설정을 수정하세요: + +```toml +[web] +port = 8000 # 웹 서버 포트 변경 +api_base_url = "http://localhost:8080" # API URL도 함께 변경해야 함 + +[api] +port = 8080 # API 서버 포트 변경 + +[mysql] +port = 13307 # 필요시 MySQL 포트 변경 +``` + +### OpenSearch 활성화 + +고급 검색 기능을 위해 OpenSearch를 활성화하려면: + +```toml +[api.opensearch] +enabled = true +``` + +**주의사항**: + +- OpenSearch는 최소 2GB의 사용 가능한 메모리가 필요합니다 +- OpenSearch 컨테이너는 `http://localhost:9200`에서 사용할 수 있습니다 +- OpenSearch 상태 확인: `http://localhost:9200/_cluster/health` + +### SMTP 설정 + +개발 환경에서는 기본 `smtp4dev` 설정을 권장합니다: + +```toml +[api.smtp] +host = "smtp4dev" +port = 25 +sender = "dev@feedback.local" +``` + +smtp4dev 웹 인터페이스는 `http://localhost:5080`에서 전송된 이메일을 확인할 수 있습니다. + +## 문제 해결 + +### 일반적인 문제 + +1. **Docker 관련 오류**: + + - Docker가 실행 중인지 확인: `docker --version` + - Docker 권한 확인: `docker ps` + - Docker Desktop이 올바르게 설치되고 실행 중인지 확인 + +2. **포트 충돌**: + + - 포트 사용 확인: `lsof -i :PORT` (macOS/Linux) 또는 `netstat -ano | findstr :PORT` (Windows) + - `config.toml`에서 포트 설정 변경 + - 일반적인 충돌 포트: 3000, 4000, 13306, 9200, 5080 + +3. **서비스 시작 실패**: + + - 컨테이너 로그 확인: `docker compose logs SERVICE_NAME` + - Docker 이미지 사용 가능 여부 확인: `docker images` + - 충분한 시스템 리소스(메모리, 디스크 공간) 확인 + +4. **데이터베이스 연결 문제**: + - MySQL 컨테이너 상태 확인: `docker compose ps mysql` + - MySQL 로그 확인: `docker compose logs mysql` + - 연결 테스트: `docker compose exec mysql mysql -u userfeedback -p` + +### 디버깅 팁 + +1. **컨테이너 로그 확인**: + + ```bash + # 모든 컨테이너 로그 + docker compose logs + + # 특정 서비스 로그 + docker compose logs api + docker compose logs web + docker compose logs mysql + ``` + +2. **서비스 상태 확인**: + + ```bash + # API 상태 확인 + curl http://localhost:4000/api/health + + # OpenSearch 상태 확인 (활성화된 경우) + curl http://localhost:9200/_cluster/health + ``` + +3. **데이터베이스 직접 접근**: + ```bash + # MySQL 연결 + docker compose exec mysql mysql -u userfeedback -p userfeedback + ``` + +## 제한사항 + +CLI 도구는 개발 및 테스트 환경을 위해 설계되었습니다. 프로덕션 배포를 위해서는 다음 사항을 고려하세요: + +1. **보안 고려사항**: + + - 민감한 데이터에 대해서는 설정 파일 대신 환경 변수 사용 + - 적절한 비밀 관리 구현 + - 프로덕션급 JWT 비밀 사용 + - HTTPS/TLS 암호화 활성화 + +2. **확장성 및 가용성**: + + - Kubernetes 또는 Docker Swarm과 같은 오케스트레이션 도구 사용 + - 로드 밸런싱 및 자동 스케일링 구현 + - 적절한 모니터링 및 알림 설정 + - 관리형 데이터베이스 서비스(RDS, Cloud SQL 등) 사용 + +3. **데이터 관리**: + - 자동화된 백업 전략 구현 + - 적절한 백업이 있는 영구 볼륨 사용 + - 데이터 보존 정책 고려 + - 디스크 사용량 및 성능 모니터링 + +## 다음 단계 + +자세한 API 및 웹 서버 설정 옵션은 [환경 변수 설정](./05-configuration.md) 문서를 참조하세요. diff --git a/apps/docs/docs/02-developer-guide/01-installation/03-manual-setup.md b/apps/docs/docs/02-developer-guide/01-installation/03-manual-setup.md new file mode 100644 index 000000000..39d97b9d2 --- /dev/null +++ b/apps/docs/docs/02-developer-guide/01-installation/03-manual-setup.md @@ -0,0 +1,276 @@ +--- +sidebar_position: 3 +title: '수동 설치' +description: '소스 코드에서 직접 ABC User Feedback을 빌드하고 실행하는 수동 설치 가이드' +--- + +# 수동 설치 + +이 문서는 ABC User Feedback을 수동으로 설치하고 구성하는 방법을 설명합니다. 소스 코드에서 직접 애플리케이션을 빌드하고 실행하고 싶을 때 유용합니다. + +## 사전 요구사항 + +수동 설치를 진행하기 전에 다음 요구사항을 충족해야 합니다: + +- [Node.js v22.19.0 이상](https://nodejs.org/en/download/) +- [pnpm v10.15.0 이상](https://pnpm.io/installation) (패키지 매니저) +- [Git](https://git-scm.com/downloads) +- [MySQL 8.0](https://www.mysql.com/downloads/) +- SMTP 서버 +- (선택사항) [OpenSearch 2.16](https://opensearch.org/) + +## 소스 코드 다운로드 + +먼저 GitHub 저장소에서 ABC User Feedback 소스 코드를 클론합니다: + +```bash +git clone https://github.com/line/abc-user-feedback.git +cd abc-user-feedback +``` + +## 인프라 설정 + +ABC User Feedback은 MySQL 데이터베이스와 SMTP 서버 그리고 선택적으로 OpenSearch가 필요합니다. 이러한 인프라 구성 요소를 설정하는 방법은 여러 가지가 있습니다. + +### Docker를 사용한 인프라 설정 + +가장 간단한 방법은 Docker Compose로 필요한 인프라를 설정하는 것입니다: + +```bash +docker-compose -f docker/docker-compose.infra.yml up -d +``` + +### 기존 인프라 사용 + +이미 MySQL, OpenSearch 또는 SMTP 서버가 있다면, 나중에 환경 변수로 연결 정보를 구성할 수 있습니다. + +## 의존성 설치 + +ABC User Feedback은 모노레포 구조를 사용하며 TurboRepo를 통해 관리됩니다. 모든 패키지의 의존성을 설치하려면: + +```bash +pnpm install +``` + +의존성 설치 후, 모든 패키지를 빌드합니다: + +```bash +pnpm build +``` + +## 환경 변수 설정 + +### API 서버 환경 변수 + +`apps/api` 디렉토리에 `.env` 파일을 생성하고 `.env.example`을 참조하여 구성합니다: + +```env +# Required environment variables +JWT_SECRET=DEV + +MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@localhost:13306/userfeedback # required + +ACCESS_TOKEN_EXPIRED_TIME=10m # default: 10m +REFRESH_TOKEN_EXPIRED_TIME=1h # default: 1h + +# Optional environment variables + +# APP_PORT=4000 # default: 4000 +# APP_ADDRESS=0.0.0.0 # default: 0.0.0.0 + +# MYSQL_SECONDARY_URLS= ["mysql://userfeedback:userfeedback@localhost:13306/userfeedback"] # optional + +SMTP_HOST=localhost # required +SMTP_PORT=25 # required +SMTP_SENDER=user@feedback.com # required +# SMTP_USERNAME= # optional +# SMTP_PASSWORD= # optional +# SMTP_TLS= # default: false +# SMTP_CIPHER_SPEC= # default: TLSv1.2 if SMTP_TLS=true +# SMTP_OPPORTUNISTIC_TLS= # default: true if SMTP_TLS=true + +# OPENSEARCH_USE=false # default: false +# OPENSEARCH_NODE= # required if OPENSEARCH_USE=true +# OPENSEARCH_USERNAME= # optional +# OPENSEARCH_PASSWORD= # optional + +# AUTO_MIGRATION=true # default: true + +# MASTER_API_KEY= # default: none + +# BASE_URL=http://localhost:4000 + +# AUTO_FEEDBACK_DELETION_ENABLED=false # default: false +# AUTO_FEEDBACK_DELETION_PERIOD_DAYS=365*5 +``` + +### Web 서버 환경 변수 + +`apps/web` 디렉토리에 `.env` 파일을 생성하고 `.env.example`을 참조하여 구성합니다: + +```env +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +``` + +환경 변수 자세한 정보는 [환경 변수 설정](./05-configuration.md) 문서를 참조하세요. + +## 데이터베이스 마이그레이션 + +API 서버를 처음 실행하기 전에 데이터베이스 스키마를 생성해야 합니다. `AUTO_MIGRATION=true` 환경 변수를 설정하면 서버 시작 시 마이그레이션이 자동으로 실행됩니다. + +수동으로 마이그레이션을 실행하려면: + +```bash +cd apps/api +npm run migration:run +``` + +## 개발 모드 실행 + +### 단일 명령어로 실행 + +API 서버와 웹 서버를 개발 모드로 실행하려면: + +```bash +# 프로젝트 루트 디렉토리에서 +pnpm dev +``` + +이 명령어는 API 서버와 웹 서버를 동시에 시작합니다. API 서버는 기본적으로 포트 4000에서, 웹 서버는 포트 3000에서 실행됩니다. + +### 개별 패키지 실행 + +#### 공통 패키지 빌드 + +웹 애플리케이션을 실행하기 전에 공유 패키지를 빌드해야 합니다: + +```bash +# 프로젝트 루트 디렉토리에서 +cd packages/ufb-shared +pnpm build +``` + +#### UI 패키지 빌드 + +웹 애플리케이션을 실행하기 전에 UI 패키지들을 빌드해야 합니다: + +```bash +# 프로젝트 루트 디렉토리에서 +cd packages/ufb-tailwindcss +pnpm build +``` + +#### 각 서버 개별 실행 + +각 서버를 개별적으로 실행하려면: + +```bash +# API 서버만 실행 +cd apps/api +pnpm dev + +# 웹 서버만 실행 +cd apps/web +pnpm dev +``` + +## 프로덕션 빌드 + +프로덕션 환경을 위한 애플리케이션을 빌드하려면: + +```bash +# 프로젝트 루트 디렉토리에서 +pnpm build +``` + +이 명령어는 API 서버와 웹 서버를 모두 빌드합니다. + +## 프로덕션 모드 실행 + +프로덕션 빌드를 실행하려면: + +```bash +# API 서버 실행 +cd apps/api +pnpm start + +# 웹 서버 실행 +cd apps/web +pnpm start +``` + +## API 타입 생성 + +백엔드 API가 실행 중일 때 프론트엔드용 API 타입을 생성할 수 있습니다: + +```bash +cd apps/web +pnpm generate-api-type +``` + +이 명령어는 OpenAPI 명세로 TypeScript 타입을 생성하고 `src/shared/types/api.type.ts` 파일에 저장합니다. + +**참고**: 이 명령어가 제대로 작동하려면 API 서버가 `http://localhost:4000`에서 실행 중이어야 합니다. + +## 코드 품질 관리 + +### 린팅 + +코드 린팅을 실행하려면: + +```bash +pnpm lint +``` + +### 포맷팅 + +코드 포맷팅을 실행하려면: + +```bash +pnpm format +``` + +### 테스트 + +테스트를 실행하려면: + +```bash +pnpm test +``` + +## Swagger 문서 + +API 서버가 실행 중일 때 다음 엔드포인트에서 Swagger 문서를 확인할 수 있습니다: + +- **API 문서**: http://localhost:4000/docs +- **관리자 API 문서**: http://localhost:4000/admin-docs +- **OpenAPI JSON**: http://localhost:4000/docs-json +- **관리자 OpenAPI JSON**: http://localhost:4000/admin-docs-json + +> **참고**: API 서버를 리버스 프록시 뒤에서 다른 URL로 서빙하는 경우, `BASE_URL` 환경 변수를 설정하면 Swagger 문서에서 올바른 API 엔드포인트 URL이 생성됩니다. 예: `BASE_URL=https://api.example.com` + +## 문제 해결 + +### 일반적인 문제 + +1. **의존성 설치 오류**: + - Node.js 버전이 v22.19.0 이상인지 확인하세요. + - pnpm 버전이 v10.15.0 이상인지 확인하세요. + - pnpm을 최신 버전으로 업데이트하세요. + - `pnpm install --force`를 시도해보세요. + +2. **데이터베이스 연결 오류**: + - MySQL 서버가 실행 중인지 확인하세요. + - 데이터베이스 자격 증명이 올바른지 확인하세요. + - `MYSQL_PRIMARY_URL` 환경 변수 형식이 올바른지 확인하세요. + - Docker 인프라를 사용하는 경우 MySQL이 포트 13306(3306이 아님)에서 실행되는지 확인하세요. + +3. **빌드 오류**: + - UI 패키지가 빌드되었는지 확인하세요 (`pnpm build:ui`). + - 모든 의존성이 설치되었는지 확인하세요. + - TypeScript 오류를 확인하세요. + +4. **런타임 오류**: + - 환경 변수가 올바르게 설정되었는지 확인하세요. + - 필요한 포트가 사용 가능한지 확인하세요. + - 로그의 오류 메시지를 확인하세요. diff --git a/apps/docs/docs/02-developer-guide/01-installation/04-smtp-configuration.md b/apps/docs/docs/02-developer-guide/01-installation/04-smtp-configuration.md new file mode 100644 index 000000000..0be73a36f --- /dev/null +++ b/apps/docs/docs/02-developer-guide/01-installation/04-smtp-configuration.md @@ -0,0 +1,162 @@ +--- +id: smtp-configuration +title: SMTP 서버 연동 가이드 +description: 운영 환경에서 인증 메일 발송을 위한 외부 SMTP 서버 연동 방법을 안내합니다. +sidebar_position: 4 +--- + +# SMTP 서버 연동 가이드 + +운영 환경에서는 `smtp4dev`와 같은 로컬 테스트 서버 대신, +**외부 SMTP 서버(Gmail, SendGrid, 회사 SMTP 등)** 와 연결하여 +인증 메일(가입, 비밀번호 재설정 등)을 정상적으로 발송할 수 있어야 합니다. + +이 문서에서는 SMTP 서버 연동을 위한 환경 변수 설정과 주요 연동 사례를 안내합니다. + +--- + +## 1. SMTP 관련 환경 변수 + +`api` 서비스 또는 `.env` 파일에 다음 환경변수를 설정하세요: + +> **참고**: 인증이 필요하지 않은 SMTP 서버의 경우 `SMTP_USERNAME`과 `SMTP_PASSWORD`는 생략할 수 있습니다. + +| 환경 변수 | 설명 | 필수 여부 | +| ------------------------ | ----------------------------------------------- | --------- | +| `SMTP_HOST` | SMTP 서버 주소 (예: smtp.gmail.com) | 필수 | +| `SMTP_PORT` | 포트 번호 (보통 587, 465 등) | 필수 | +| `SMTP_SENDER` | 발신 이메일 주소 (예: `noreply@yourdomain.com`) | 필수 | +| `SMTP_USERNAME` | SMTP 인증 사용자명 (계정 ID) | 선택 | +| `SMTP_PASSWORD` | SMTP 인증 비밀번호 또는 API 키 | 선택 | +| `SMTP_TLS` | TLS 사용 여부 (`true` 또는 `false`) | 선택 | +| `SMTP_CIPHER_SPEC` | TLS 암호화 알고리즘 (기본값: `TLSv1.2`) | 선택 | +| `SMTP_OPPORTUNISTIC_TLS` | STARTTLS 사용 여부 (`true` 또는 `false`) | 선택 | + +> **중요**: 실제 코드에서는 `SMTP_USERNAME`과 `SMTP_PASSWORD`를 사용하며, `SMTP_TLS=true`는 포트 465에, `false`는 포트 587에 주로 사용됩니다. + +--- + +## 2. Docker 환경 예시 + +```yaml +api: + image: line/abc-user-feedback-api + environment: + - SMTP_HOST=smtp.gmail.com + - SMTP_PORT=587 + - SMTP_USERNAME=your-email@gmail.com + - SMTP_PASSWORD=your-email-app-password + - SMTP_SENDER=noreply@yourdomain.com + - SMTP_TLS=false + - SMTP_OPPORTUNISTIC_TLS=true +``` + +또는 `.env` 파일로 분리 관리할 수 있습니다: + +```env +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME=your-email@gmail.com +SMTP_PASSWORD=your-email-app-password +SMTP_SENDER=noreply@yourdomain.com +SMTP_TLS=false +SMTP_OPPORTUNISTIC_TLS=true +``` + +--- + +## 3. SMTP 연동 예시 + +### ✅ Gmail SMTP 연동 (개인 테스트용) + +- `SMTP_HOST`: `smtp.gmail.com` +- `SMTP_PORT`: `587` +- `SMTP_USERNAME`: Gmail 주소 (예: `abc@gmail.com`) +- `SMTP_PASSWORD`: **앱 비밀번호** (보안 수준 낮은 앱 허용 → 비권장) +- `SMTP_TLS`: `false` +- `SMTP_OPPORTUNISTIC_TLS`: `true` + +> Gmail 계정에 **2단계 인증**이 활성화된 경우 [앱 비밀번호](https://myaccount.google.com/apppasswords)를 생성해야 합니다. + +--- + +### ✅ SendGrid 연동 (권장) + +- `SMTP_HOST`: `smtp.sendgrid.net` +- `SMTP_PORT`: `587` +- `SMTP_USERNAME`: `apikey` +- `SMTP_PASSWORD`: 실제 SendGrid API Key +- `SMTP_SENDER`: verified sender 주소 +- `SMTP_TLS`: `false` +- `SMTP_OPPORTUNISTIC_TLS`: `true` + +--- + +## 4. 테스트 방법 + +### 4.1 메일 발송 테스트 + +1. **이메일 인증 테스트**: + - 관리자 또는 사용자 계정 생성 + - 이메일 인증 코드 발송 확인 + +2. **비밀번호 재설정 테스트**: + - 비밀번호 재설정 요청 + - 재설정 링크가 포함된 메일 수신 확인 + +3. **사용자 초대 테스트**: + - 관리자가 새 사용자 초대 + - 초대 메일 발송 확인 + +### 4.2 로그 확인 + +메일 발송 실패 시 다음 명령어로 상세 로그를 확인하세요: + +```bash +# Docker Compose 환경 +docker compose logs api + +# 특정 시간대 로그 확인 +docker compose logs --since=10m api + +# 실시간 로그 모니터링 +docker compose logs -f api +``` + +SMTP 오류가 발생하면 로그에 상세 메시지가 표시됩니다. + +--- + +## 5. 문제 해결 (Troubleshooting) + +| 문제 유형 | 원인 또는 해결 방법 | +| -------------------------- | ---------------------------------------- | +| 인증 오류 (`535`) | `SMTP_USERNAME` / `SMTP_PASSWORD` 재확인 | +| 연결 거부 (`ECONNREFUSED`) | 방화벽 또는 잘못된 포트 설정 | +| 메일 안 도착 | `SMTP_SENDER`가 인증되지 않음 | +| TLS 오류 (`ETLS`) | `SMTP_TLS` 설정이 잘못됨 | +| STARTTLS 실패 | `SMTP_OPPORTUNISTIC_TLS` 설정 확인 | + +--- + +## 6. SMTP와 관련된 메일 템플릿 + +현재 시스템에서 메일은 다음 상황에서 발송됩니다: + +- **이메일 인증**: 관리자/사용자 가입 시 인증 코드 발송 +- **비밀번호 재설정**: 비밀번호 재설정 요청 시 링크 발송 +- **사용자 초대**: 관리자가 사용자를 초대할 때 초대 메일 발송 + +메일 내용은 **Handlebars 템플릿** 기반으로 구성되어 있으며, 다음 정보가 포함됩니다: + +- 발신자: `"User feedback" ` +- 기본 URL: `ADMIN_WEB_URL` 환경변수 값 사용 +- 템플릿 위치: `src/configs/modules/mailer-config/templates/` + +--- + +## 관련 문서 + +- [Docker Hub 설치 가이드](./01-docker-hub-images.md) +- [환경 변수 설정](./05-configuration.md) +- [초기 셋팅 가이드](/docs/01-user-guide/01-getting-started.md) diff --git a/apps/docs/docs/02-developer-guide/01-installation/05-configuration.md b/apps/docs/docs/02-developer-guide/01-installation/05-configuration.md new file mode 100644 index 000000000..cc4d3f8e7 --- /dev/null +++ b/apps/docs/docs/02-developer-guide/01-installation/05-configuration.md @@ -0,0 +1,159 @@ +--- +id: configuration +title: 환경 변수 구성 +description: ABC User Feedback의 API 및 웹 서버 환경 변수 구성 방법을 설명합니다. +sidebar_position: 5 +--- + +# 환경 변수 구성 + +이 문서에서는 ABC User Feedback의 **API 서버** 및 **웹 서버**에서 사용하는 주요 환경 변수와 설정 방법을 설명합니다. + +--- + +## 1. API 서버 환경 변수 + +### 필수 환경 변수 + +| 환경 변수 | 설명 | 기본값 | 예시 | +| ---------------------------- | ------------------------- | ------ | -------------------------------- | +| `JWT_SECRET` | JWT 서명을 위한 시크릿 키 | 없음 | `jwtsecretjwtsecretjwtsecret` | +| `MYSQL_PRIMARY_URL` | MySQL 연결 URL | 없음 | `mysql://user:pass@host:3306/db` | +| `ACCESS_TOKEN_EXPIRED_TIME` | Access Token 유효 시간 | `10m` | `10m`, `30s`, `1h` | +| `REFRESH_TOKEN_EXPIRED_TIME` | Refresh Token 유효 시간 | `1h` | `1h`, `7d` | + +> JWT 시크릿은 충분히 복잡하고 안전한 문자열을 사용해야 합니다. + +⚠️ **보안 주의사항**: + +- `JWT_SECRET`은 최소 32자 이상의 복잡한 문자열을 사용하세요 +- 프로덕션 환경에서는 절대 기본값을 사용하지 마세요 +- 환경 변수 파일(`.env`)은 버전 관리에 포함하지 마세요 +- 민감한 정보는 환경 변수나 시크릿 관리 시스템을 통해 관리하세요 + +--- + +### 선택 환경 변수 + +| 환경 변수 | 설명 | 기본값 | 예시 | +| ---------------------- | ------------------------------------------- | ----------------------- | --------------------------- | +| `APP_PORT` | API 서버 포트 | `4000` | `4000` | +| `APP_ADDRESS` | 바인딩 주소 | `0.0.0.0` | `127.0.0.1` | +| `ADMIN_WEB_URL` | 관리자 웹 URL | `http://localhost:3000` | `https://admin.company.com` | +| `BASE_URL` | API 서버의 공개 URL (Swagger 문서에서 사용) | 없음 | `https://api.example.com` | +| `MYSQL_SECONDARY_URLS` | 보조 DB URL (JSON 배열) | 없음 | `["mysql://..."]` | +| `AUTO_MIGRATION` | 앱 시작 시 DB 자동 마이그레이션 | `true` | `false` | +| `MASTER_API_KEY` | 마스터 권한 API 키 (선택) | 없음 | `abc123xyz` | +| `NODE_OPTIONS` | Node 실행 옵션 | 없음 | `--max_old_space_size=4096` | + +--- + +### SMTP 설정 (이메일 인증) + +| 환경 변수 | 설명 | 예시 | +| ------------------------ | ------------------------- | ------------------------------ | +| `SMTP_HOST` | SMTP 서버 주소 | `smtp.gmail.com` | +| `SMTP_PORT` | 포트 (보통 587 또는 465) | `587` | +| `SMTP_USERNAME` | 로그인 사용자 | `user@example.com` | +| `SMTP_PASSWORD` | 로그인 비밀번호 또는 토큰 | `app-password` | +| `SMTP_SENDER` | 발신자 주소 | `noreply@company.com` | +| `SMTP_BASE_URL` | 메일 내 링크용 기본 URL | `https://feedback.company.com` | +| `SMTP_TLS` | TLS 사용 여부 | `true` | +| `SMTP_CIPHER_SPEC` | 암호화 스펙 | `TLSv1.2` | +| `SMTP_OPPORTUNISTIC_TLS` | STARTTLS 지원 여부 | `true` | + +📎 자세한 설정은 [SMTP 연동 가이드](./04-smtp-configuration.md)를 참고하세요. + +--- + +## 2. OpenSearch 설정 (선택) + +| 환경 변수 | 설명 | 예시 | +| --------------------- | ---------------------- | ----------------------- | +| `OPENSEARCH_USE` | OpenSearch 활성화 여부 | `true` | +| `OPENSEARCH_NODE` | OpenSearch 노드 URL | `http://localhost:9200` | +| `OPENSEARCH_USERNAME` | 인증 ID | `admin` | +| `OPENSEARCH_PASSWORD` | 인증 비밀번호 | `admin123` | + +> OpenSearch는 검색 속도 향상 및 AI 기능 개선에 사용됩니다. + +--- + +## 3. 자동 피드백 삭제 설정 + +| 환경 변수 | 설명 | 기본값 / 조건 | +| ------------------------------------ | ------------------------------ | ----------------------- | +| `AUTO_FEEDBACK_DELETION_ENABLED` | 오래된 피드백 삭제 기능 활성화 | `false` | +| `AUTO_FEEDBACK_DELETION_PERIOD_DAYS` | 삭제 기준 일수 | `365` (필수 if enabled) | + +--- + +## 4. 웹 서버 환경 변수 + +### 필수 환경 변수 + +| 환경 변수 | 설명 | 예시 | +| -------------------------- | ----------------------------------- | ----------------------- | +| `NEXT_PUBLIC_API_BASE_URL` | 클라이언트에서 사용할 API 서버 주소 | `http://localhost:4000` | + +### 선택 환경 변수 + +| 환경 변수 | 설명 | 기본값 | 예시 | +| --------- | --------------- | ------ | ------ | +| `PORT` | 프론트엔드 포트 | `3000` | `3000` | + +--- + +## 5. 설정 방법 + +### Docker Compose 예시 + +```yaml +services: + api: + image: line/abc-user-feedback-api + environment: + - JWT_SECRET=changeme + - MYSQL_PRIMARY_URL=mysql://user:pass@mysql:3306/userfeedback + - SMTP_HOST=smtp.sendgrid.net + - SMTP_USERNAME=apikey + - SMTP_PASSWORD=your-sendgrid-key +``` + +### .env 파일 예시 + +``` +# apps/api/.env +JWT_SECRET=changemechangemechangeme +MYSQL_PRIMARY_URL=mysql://root:pass@localhost:3306/db +ACCESS_TOKEN_EXPIRED_TIME=10m +REFRESH_TOKEN_EXPIRED_TIME=1h +SMTP_HOST=smtp.example.com +SMTP_SENDER=noreply@example.com +# BASE_URL=https://api.example.com # 리버스 프록시 뒤에서 서빙하는 경우 설정 + +# apps/web/.env +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +``` + +--- + +## 7. 문제 해결 가이드 + +| 문제 | 원인 및 해결책 | +| ------------------------- | -------------------------------------------- | +| 환경 변수가 인식되지 않음 | `.env` 위치 확인 또는 컨테이너 재시작 | +| DB 연결 실패 | `MYSQL_PRIMARY_URL` 형식 또는 연결 정보 확인 | +| SMTP 오류 | 포트/TLS 설정 또는 인증 정보 재확인 | +| OpenSearch 오류 | 노드 URL 또는 사용자 인증 확인 | +| JWT 토큰 오류 | `JWT_SECRET` 길이 및 복잡성 확인 | +| 환경 변수 검증 실패 | 필수 환경 변수 누락 또는 타입 오류 확인 | +| 포트 충돌 | `APP_PORT`, `PORT` 설정 확인 | + +--- + +## 관련 문서 + +- [Docker 설치 가이드](./01-docker-hub-images.md) +- [SMTP 연동 가이드](./04-smtp-configuration.md) +- [초기 셋팅 가이드](/docs/01-user-guide/01-getting-started.md) diff --git a/apps/docs/docs/02-developer-guide/01-installation/_category_.json b/apps/docs/docs/02-developer-guide/01-installation/_category_.json new file mode 100644 index 000000000..adb1fcf9f --- /dev/null +++ b/apps/docs/docs/02-developer-guide/01-installation/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 1, + "label": "설치", + "description": "개발 환경 설정 및 설치 가이드입니다." +} diff --git a/apps/docs/docs/02-developer-guide/01-installation/index.md b/apps/docs/docs/02-developer-guide/01-installation/index.md new file mode 100644 index 000000000..54f507ec4 --- /dev/null +++ b/apps/docs/docs/02-developer-guide/01-installation/index.md @@ -0,0 +1,7 @@ +--- +title: 설치 +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/docs/02-developer-guide/02-api-integration.md b/apps/docs/docs/02-developer-guide/02-api-integration.md new file mode 100644 index 000000000..f3ecffced --- /dev/null +++ b/apps/docs/docs/02-developer-guide/02-api-integration.md @@ -0,0 +1,532 @@ +--- +sidebar_position: 2 +title: "API 연동" +description: "ABC User Feedback API를 활용한 외부 시스템 연동 방법과 실제 구현 예시를 안내합니다." +--- + +# API 연동 + +ABC User Feedback은 **RESTful API**를 통해 외부 시스템과 연동할 수 있습니다. 프로그래매틱하게 피드백을 수집하고, 이슈를 관리하며, 데이터를 조회할 수 있어 기존 서비스나 워크플로우에 쉽게 통합할 수 있습니다. + +--- + +## API 기본 정보 + +### 공식 API 문서 + +ABC User Feedback의 **완전한 API 문서**는 다음 링크에서 확인할 수 있습니다: + +🔗 **[공식 API 문서 (Redocly)](https://line.github.io/abc-user-feedback/)** + +이 문서에서는 모든 엔드포인트의 상세한 스펙, 요청/응답 예제, 그리고 실제 테스트가 가능한 인터페이스를 제공합니다. + +### Base URL + +``` +https://your-domain.com/api +``` + +### 인증 방식 + +모든 API 요청은 **API 키 기반 인증**을 사용합니다. + +```http +X-API-KEY: your-api-key-here +Content-Type: application/json +``` + +:::warning 보안 주의사항 +API 키는 서버 사이드에서만 사용하고, 클라이언트(브라우저, 모바일 앱)에 노출하지 마세요. +::: + +### API 키 발급 방법 + +1. **관리자 페이지 접속**: ABC User Feedback 관리자 페이지에 로그인 +2. **프로젝트 설정**: 해당 프로젝트의 설정 페이지로 이동 +3. **API 키 관리**: "API 키 관리" 메뉴에서 새 API 키 생성 +4. **키 복사**: 생성된 API 키를 안전한 곳에 저장 + +:::info API 키 권한 +API 키는 프로젝트별로 발급되며, 해당 프로젝트의 데이터에만 접근할 수 있습니다. +::: + +--- + +## 주요 API 엔드포인트 예제 + +### 1. 피드백 생성 + +#### 기본 피드백 생성 + +```javascript +const createFeedback = async ( + projectId, + channelId, + message, + issueNames = [] +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + message: message, + issueNames: issueNames, + }), + } + ); + + return await response.json(); +}; + +// 사용 예제 +const feedback = await createFeedback(1, 1, "결제 오류가 발생했습니다", [ + "결제", + "오류", +]); +``` + +### 2. 피드백 조회 + +#### 채널별 피드백 검색 + +```javascript +const searchFeedbacks = async ( + projectId, + channelId, + searchText, + limit = 10, + page = 1 +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/search`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + limit: limit, + page: page, + query: { + searchText: searchText, + createdAt: { + gte: "2024-01-01", + lt: "2024-12-31", + }, + }, + sort: { + createdAt: "DESC", + }, + }), + } + ); + + return await response.json(); +}; + +// 사용 예제 +const feedbacks = await searchFeedbacks(1, 1, "결제", 20, 1); +console.log( + `총 ${feedbacks.meta.totalItems}개의 피드백 중 ${feedbacks.items.length}개 조회` +); +``` + +#### 단일 피드백 조회 + +```javascript +const getFeedbackById = async (projectId, channelId, feedbackId) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, + { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 사용 예제 +const feedback = await getFeedbackById(1, 1, 123); +console.log("피드백 상세:", feedback); +``` + +#### 피드백 업데이트 + +```javascript +const updateFeedback = async (projectId, channelId, feedbackId, updateData) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify(updateData), + } + ); + + return await response.json(); +}; + +// 사용 예제 +const updatedFeedback = await updateFeedback(1, 1, 123, { + message: "수정된 피드백 내용", + issueNames: ["수정된 이슈"], +}); +``` + +#### 피드백 삭제 + +```javascript +const deleteFeedbacks = async (projectId, channelId, feedbackIds) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + feedbackIds: feedbackIds, + }), + } + ); + + return await response.json(); +}; + +// 사용 예제 +const result = await deleteFeedbacks(1, 1, [123, 124, 125]); +console.log("삭제 완료:", result); +``` + +### 3. 이슈 관리 + +#### 이슈 생성 + +```javascript +const createIssue = async (projectId, name, description) => { + const response = await fetch(`/api/projects/${projectId}/issues`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + name: name, + description: description, + }), + }); + + return await response.json(); +}; + +// 사용 예제 +const issue = await createIssue( + 1, + "결제 오류", + "사용자가 결제 과정에서 오류를 경험함" +); +``` + +#### 이슈 검색 + +```javascript +const searchIssues = async (projectId, query = {}) => { + const response = await fetch(`/api/projects/${projectId}/issues/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + limit: 10, + page: 1, + query: query, + sort: { + createdAt: "DESC", + }, + }), + }); + + return await response.json(); +}; + +// 사용 예제 +const issues = await searchIssues(1, { name: "결제" }); +``` + +#### 이슈 조회 + +```javascript +const getIssueById = async (projectId, issueId) => { + const response = await fetch(`/api/projects/${projectId}/issues/${issueId}`, { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + }); + + return await response.json(); +}; + +// 사용 예제 +const issue = await getIssueById(1, 123); +console.log("이슈 상세:", issue); +``` + +#### 이슈 업데이트 + +```javascript +const updateIssue = async (projectId, issueId, updateData) => { + const response = await fetch(`/api/projects/${projectId}/issues/${issueId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify(updateData), + }); + + return await response.json(); +}; + +// 사용 예제 +const updatedIssue = await updateIssue(1, 123, { + name: "수정된 이슈명", + description: "수정된 이슈 설명", +}); +``` + +#### 이슈 삭제 + +```javascript +const deleteIssues = async (projectId, issueIds) => { + const response = await fetch(`/api/projects/${projectId}/issues`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + issueIds: issueIds, + }), + }); + + return await response.json(); +}; + +// 사용 예제 +const result = await deleteIssues(1, [123, 124, 125]); +console.log("이슈 삭제 완료:", result); +``` + +#### 피드백에 이슈 추가 + +```javascript +const addIssueToFeedback = async ( + projectId, + channelId, + feedbackId, + issueId +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}/issues/${issueId}`, + { + method: "POST", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 사용 예제 +const result = await addIssueToFeedback(1, 1, 123, 456); +console.log("이슈 추가 완료:", result); +``` + +#### 피드백에서 이슈 제거 + +```javascript +const removeIssueFromFeedback = async ( + projectId, + channelId, + feedbackId, + issueId +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}/issues/${issueId}`, + { + method: "DELETE", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 사용 예제 +const result = await removeIssueFromFeedback(1, 1, 123, 456); +console.log("이슈 제거 완료:", result); +``` + +### 4. 프로젝트 및 채널 정보 + +#### 프로젝트 정보 조회 + +```javascript +const getProjectInfo = async (projectId) => { + const response = await fetch(`/api/projects/${projectId}`, { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + }); + + return await response.json(); +}; + +// 사용 예제 +const project = await getProjectInfo(1); +console.log("프로젝트 정보:", project); +``` + +#### 채널 필드 조회 + +```javascript +const getChannelFields = async (projectId, channelId) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/fields`, + { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 사용 예제 +const fields = await getChannelFields(1, 1); +console.log("채널 필드:", fields); +``` + +--- + +## Swagger를 통한 API 테스트 + +ABC User Feedback은 **Swagger UI**를 제공하여 API를 쉽게 테스트하고 이해할 수 있습니다. + +### Swagger 접근 방법 + +**API 서버 주소 + `/docs`** 로 접속합니다: + +``` +https://your-domain.com/api/docs +``` + +또는 **ReDoc 형식**으로: + +``` +https://your-domain.com/api/docs/redoc +``` + +### Swagger에서 API 키 설정 + +1. Swagger UI 상단의 **"Authorize"** 버튼 클릭 +2. **X-API-KEY** 필드에 발급받은 API 키 입력 +3. **"Authorize"** 클릭으로 인증 완료 + +이후 모든 API 요청에서 자동으로 API 키가 포함되어 테스트할 수 있습니다. + +### Swagger 활용 팁 + +- **"Try it out"** 버튼으로 실제 API 호출 테스트 +- **Response body** 섹션에서 실제 응답 데이터 구조 확인 +- **Schema** 탭에서 요청/응답 데이터 형식 상세 확인 +- **cURL** 명령어 자동 생성으로 CLI 테스트 가능 + +--- + +## 에러 처리 및 재시도 로직 + +### HTTP 상태 코드 + +| 상태 코드 | 의미 | 처리 방법 | +| --------- | -------------- | ----------------------- | +| **200** | 성공 | 정상 처리 | +| **400** | 잘못된 요청 | 요청 데이터 검증 | +| **401** | 인증 실패 | API 키 확인 | +| **403** | 권한 없음 | 프로젝트 접근 권한 확인 | +| **404** | 리소스 없음 | ID 값 확인 | +| **429** | 요청 한도 초과 | 잠시 후 재시도 | +| **500** | 서버 오류 | 재시도 또는 지원팀 문의 | + +## 응답 데이터 파싱 방법 + +### 페이지네이션 응답 구조 + +```json +{ + "meta": { + "itemCount": 10, + "totalItems": 100, + "itemsPerPage": 10, + "totalPages": 10, + "currentPage": 1 + }, + "items": [ + { + "id": 1, + "message": "피드백 내용", + "createdAt": "2024-01-01T00:00:00.000Z", + "issues": [ + { + "id": 1, + "name": "이슈명" + } + ] + } + ] +} +``` + +## 보안 및 성능 최적화 + +### API 키 보안 + +- **환경 변수 사용**: API 키를 환경 변수로 관리 +- **서버 사이드만**: 클라이언트에 API 키 노출 금지 +- **키 로테이션**: 정기적인 API 키 교체 +- **IP 화이트리스트**: 가능한 경우 특정 IP에서만 접근 허용 + +### 성능 최적화 + +- **페이지네이션 활용**: 대량 데이터 조회 시 적절한 limit 설정 +- **필요한 필드만 요청**: 쿼리 최적화로 응답 속도 개선 +- **캐싱 전략**: 자주 조회하는 데이터는 클라이언트 사이드 캐싱 +- **배치 처리**: 여러 요청을 묶어서 처리 + +## 관련 문서 + +- [API 키 관리](/docs/01-user-guide/07-settings/02-api-key-management.md) - UI에서 API 키 발급하는 방법 +- [이미지 설정](/docs/01-user-guide/07-settings/06-image-setting.md) - 이미지 업로드 API 사용을 위한 설정 +- [Webhook 연동](/docs/01-user-guide/07-settings/04-webhook-management.md) - API와 함께 활용할 수 있는 실시간 알림 설정 diff --git a/apps/docs/docs/02-developer-guide/03-oauth-integration.md b/apps/docs/docs/02-developer-guide/03-oauth-integration.md new file mode 100644 index 000000000..bbecdecbe --- /dev/null +++ b/apps/docs/docs/02-developer-guide/03-oauth-integration.md @@ -0,0 +1,171 @@ +--- +sidebar_position: 3 +title: "OAuth 연동" +description: "Google OAuth 및 커스텀 OAuth 제공자를 통한 싱글 사인온(SSO) 연동 방법을 안내합니다." +--- + +# OAuth 연동 + +ABC User Feedback에서 OAuth 2.0 기반의 싱글 사인온(SSO)을 설정하면, 사용자들이 별도의 계정 생성 없이 기존 계정(Google, Microsoft, GitHub 등)으로 로그인할 수 있습니다. 이는 사용자 편의성을 높이고 기업 환경에서 통합 인증을 구현하는 데 필수적입니다. + +--- + +## OAuth 연동 개요 + +ABC User Feedback에서 지원하는 OAuth 방식: + +### 1. Google OAuth + +- 별도 설정 없이 기본 제공 +- Google 계정을 통한 간편 로그인 + +### 2. 커스텀 OAuth 제공자 + +- 사내 인증 시스템 +- 기타 OAuth 2.0/OpenID Connect 호환 서비스 + +OAuth를 설정하면 기존 이메일 로그인과 병행하여 사용할 수 있으며, 조직 정책에 따라 OAuth만 허용하도록 제한할 수도 있습니다. + +--- + +## Google OAuth 연동 설정 + +### Google Cloud Console에서 설정 + +#### 1. Google Cloud Console 접속 + +[Google Cloud Console](https://console.cloud.google.com)에 접속하여 프로젝트를 생성하거나 기존 프로젝트를 선택합니다. + +#### 2. OAuth 2.0 클라이언트 ID 생성 + +1. **API 및 서비스 > 사용자 인증 정보** 메뉴로 이동 +2. **+ 사용자 인증 정보 만들기 > OAuth 클라이언트 ID** 선택 +3. 애플리케이션 유형을 **웹 애플리케이션**으로 선택 + +#### 3. 승인된 리디렉션 URI 설정 + +**승인된 리디렉션 URI**에 다음 URL을 추가합니다: + +``` +https://your-domain.com/auth/oauth-callback +``` + +예시: + +- `https://feedback.company.com/auth/oauth-callback` +- `http://localhost:3000/auth/oauth-callback` (개발 환경) + +#### 4. 클라이언트 정보 확인 + +생성 완료 후 다음 정보를 확인하고 복사해둡니다: + +- **클라이언트 ID**: `1234567890-abc123def456.apps.googleusercontent.com` +- **클라이언트 보안 비밀번호**: `GOCSPX-abcdef123456` + +### ABC User Feedback에서 Google OAuth 설정 + +Google OAuth를 사용하려면 다음 단계를 따라 설정해야 합니다: + +#### 1. Google OAuth 설정 활성화 + +**Settings > Login Management**에서: + +1. **OAuth2.0 Login** 토글을 활성화 +2. **Login Button Type**을 "Google Login"으로 선택 +3. Google Cloud Console에서 얻은 정보 입력: + - **Client ID**: Google Cloud Console에서 생성한 클라이언트 ID + - **Client Secret**: Google Cloud Console에서 생성한 클라이언트 보안 비밀번호 + - **Authorization Code Request URL**: `https://accounts.google.com/o/oauth2/v2/auth` + - **Scope**: `openid email profile` + - **Access Token URL**: `https://oauth2.googleapis.com/token` + - **User Profile Request URL**: `https://www.googleapis.com/oauth2/v2/userinfo` + - **Email Key**: `email` + +#### 2. 리디렉션 URI 등록 + +Google Cloud Console에서 다음 URL을 **승인된 리디렉션 URI**에 추가: + +``` +https://your-domain.com/auth/oauth-callback +``` + +개발 환경의 경우: + +``` +http://localhost:3000/auth/oauth-callback +``` + +--- + +## 커스텀 OAuth 제공자 연동 + +### 사내 인증 시스템 연동 + +ABC User Feedback은 기업 환경에서 사용하는 사내 인증 시스템과 연동할 수 있습니다. 대부분의 사내 인증 시스템은 OAuth 2.0 또는 OpenID Connect 표준을 지원하므로, 표준 OAuth 플로우를 통해 연동이 가능합니다. + +#### 사내 인증 시스템 설정 요구사항 + +사내 인증 시스템과 연동하려면 다음 정보가 필요합니다: + +1. **OAuth 클라이언트 등록** + + - 클라이언트 ID + - 클라이언트 시크릿 + - 리디렉션 URI: `https://your-domain.com/auth/oauth-callback` + +2. **OAuth 엔드포인트 정보** + + - Authorization URL (인증 요청 URL) + - Token URL (토큰 교환 URL) + - User Info URL (사용자 정보 조회 URL) + +3. **권한 범위(Scope)** + - 사용자 프로필 정보 접근 권한 + - 이메일 주소 접근 권한 + +#### 일반적인 사내 인증 시스템 예시 + +| 항목 | 설명 | 사내 시스템 예시 | +| ---------------------------------- | ---------------------------------- | ------------------------------------------ | +| **Login Button Type** | 로그인 버튼 타입 | `CUSTOM` | +| **Login Button Name** | 로그인 버튼에 표시될 이름 | `사내 계정으로 로그인` | +| **Client ID** | OAuth 클라이언트 ID | `company-auth-client-123` | +| **Client Secret** | 클라이언트 보안 비밀번호 | `company-secret-abc123` | +| **Authorization Code Request URL** | 사용자 인증 요청 URL | `https://auth.company.com/oauth/authorize` | +| **Scope** | 요청할 권한 범위 | `openid email profile` | +| **Access Token URL** | 토큰 요청 URL | `https://auth.company.com/oauth/token` | +| **User Profile Request URL** | 사용자 정보 조회 API | `https://auth.company.com/api/user` | +| **Email Key** | 사용자 정보 JSON에서 이메일 필드명 | `email` 또는 `mail` | + +### 기타 OAuth 2.0/OpenID Connect 호환 서비스 + +ABC User Feedback은 OAuth 2.0 또는 OpenID Connect 표준을 준수하는 모든 인증 서비스와 연동할 수 있습니다. + +#### 지원 가능한 서비스 유형 + +- **OpenID Connect 제공자**: 표준 OpenID Connect 프로토콜을 지원하는 서비스 +- **OAuth 2.0 제공자**: OAuth 2.0 Authorization Code 플로우를 지원하는 서비스 +- **커스텀 인증 서버**: 표준 OAuth 엔드포인트를 제공하는 자체 구축 서비스 + +#### 연동 설정 방법 + +**Settings > Login Management**에서 커스텀 OAuth를 설정합니다: + +1. **관리자 계정으로 로그인** 후 **Settings > Login Management** 메뉴로 이동 +2. **OAuth2.0 Login** 토글을 활성화 +3. **Login Button Type**을 `CUSTOM`으로 선택 +4. 인증 서비스 제공자로부터 받은 정보 입력: + - **Login Button Name**: 로그인 버튼에 표시될 텍스트 (예: "사내 계정으로 로그인") + - **Client ID**: OAuth 클라이언트 식별자 + - **Client Secret**: 클라이언트 인증 비밀번호 + - **Authorization Code Request URL**: 사용자 인증 요청 URL + - **Scope**: 요청할 권한 범위 (공백으로 구분, 예: "openid email profile") + - **Access Token URL**: 액세스 토큰 요청 URL + - **User Profile Request URL**: 사용자 프로필 정보 조회 URL + - **Email Key**: 사용자 정보 JSON에서 이메일 필드명 (예: "email" 또는 "mail") + +--- + +## 관련 문서 + +- [로그인 관리](/docs/01-user-guide/07-settings/01-tenant-settings.md) - UI에서 OAuth 설정하는 방법 diff --git a/apps/docs/docs/02-developer-guide/04-webhook-integration.md b/apps/docs/docs/02-developer-guide/04-webhook-integration.md new file mode 100644 index 000000000..4411fa841 --- /dev/null +++ b/apps/docs/docs/02-developer-guide/04-webhook-integration.md @@ -0,0 +1,302 @@ +--- +sidebar_position: 4 +title: '웹훅 연동' +description: '웹훅을 활용하여 외부 시스템과 실시간 연동하는 방법과 구현 예시를 안내합니다.' +--- + +# 웹훅 연동 + +웹훅으로 ABC User Feedback에서 발생하는 주요 이벤트를 실시간으로 외부 시스템에 전달할 수 있습니다. Slack 알림, 자동화 워크플로우, 커스텀 분석 시스템 등과 연동할 수 있습니다. + +--- + +## 지원되는 이벤트 타입 + +ABC User Feedback에서 지원하는 이벤트는 다음과 같습니다: + +### 1. FEEDBACK_CREATION + +새로운 피드백이 생성되었을 때 발생합니다. + +**요청 헤더:** + +``` +Content-Type: application/json +x-webhook-token: your-secret-token +``` + +**페이로드 예시:** + +```json +{ + "event": "FEEDBACK_CREATION", + "data": { + "feedback": { + "id": 123, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "message": "사용자 피드백 내용", + "userEmail": "user@example.com", + "issues": [ + { + "id": 456, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "name": "버그 리포트", + "description": "이슈 설명", + "status": "OPEN", + "externalIssueId": "EXT-123", + "feedbackCount": 5 + } + ] + }, + "channel": { + "id": 1, + "name": "웹사이트 피드백" + }, + "project": { + "id": 1, + "name": "My Project" + } + } +} +``` + +### 2. ISSUE_CREATION + +새로운 이슈가 생성되었을 때 발생합니다. + +**페이로드 예시:** + +```json +{ + "event": "ISSUE_CREATION", + "data": { + "issue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T11:00:00.000Z", + "name": "새로운 이슈", + "description": "이슈 설명", + "status": "OPEN", + "externalIssueId": "EXT-789", + "feedbackCount": 0 + }, + "project": { + "id": 1, + "name": "My Project" + } + } +} +``` + +### 3. ISSUE_STATUS_CHANGE + +이슈 상태가 변경되었을 때 발생합니다. + +**페이로드 예시:** + +```json +{ + "event": "ISSUE_STATUS_CHANGE", + "data": { + "issue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T12:00:00.000Z", + "name": "이슈 이름", + "description": "이슈 설명", + "status": "IN_PROGRESS", + "externalIssueId": "EXT-789", + "feedbackCount": 3 + }, + "project": { + "id": 1, + "name": "My Project" + }, + "previousStatus": "OPEN" + } +} +``` + +### 4. ISSUE_ADDITION + +피드백에 이슈가 추가되었을 때 발생합니다. + +**페이로드 예시:** + +```json +{ + "event": "ISSUE_ADDITION", + "data": { + "feedback": { + "id": 123, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "message": "사용자 피드백 내용", + "issues": [ + { + "id": 456, + "name": "기존 이슈", + "status": "OPEN" + }, + { + "id": 789, + "name": "새로 추가된 이슈", + "status": "OPEN" + } + ] + }, + "channel": { + "id": 1, + "name": "웹사이트 피드백" + }, + "project": { + "id": 1, + "name": "My Project" + }, + "addedIssue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T11:00:00.000Z", + "name": "새로 추가된 이슈", + "description": "이슈 설명", + "status": "OPEN", + "externalIssueId": "EXT-456", + "feedbackCount": 1 + } + } +} +``` + +--- + +## 웹훅 수신 서버 구현 + +웹훅을 받기 위한 HTTP 서버를 구현해야 합니다. 서버는 다음 요구사항을 만족해야 합니다: + +### 기본 요구사항 + +1. **HTTP POST 요청 처리**: 웹훅은 HTTP POST로 전송됩니다 +2. **JSON 페이로드 파싱**: 요청 본문은 JSON 형식입니다 +3. **200 응답 코드 반환**: 처리 성공 시 반드시 200 상태 코드로 응답 + +### 구현 예시 (Node.js/Express) + +```javascript +const express = require('express'); +const app = express(); + +app.use(express.json()); + +app.post('/webhook', (req, res) => { + const { event, data } = req.body; + const token = req.headers['x-webhook-token']; + + // 토큰 검증 + if (token !== 'your-secret-token') { + return res.status(401).json({ error: 'Unauthorized' }); + } + + // 이벤트 처리 + switch (event) { + case 'FEEDBACK_CREATION': + console.log('새 피드백 생성:', data.feedback); + // 피드백 처리 로직 + break; + case 'ISSUE_CREATION': + console.log('새 이슈 생성:', data.issue); + // 이슈 처리 로직 + break; + case 'ISSUE_STATUS_CHANGE': + console.log( + '이슈 상태 변경:', + data.issue, + '이전 상태:', + data.previousStatus, + ); + // 상태 변경 처리 로직 + break; + case 'ISSUE_ADDITION': + console.log('이슈 추가:', data.addedIssue); + // 이슈 추가 처리 로직 + break; + } + + res.status(200).json({ success: true }); +}); + +app.listen(3000, () => { + console.log('웹훅 리스너 서버가 포트 3000에서 실행 중입니다.'); +}); +``` + +--- + +## 보안 및 재시도 정책 + +### 보안 고려사항 + +- **토큰 검증**: `x-webhook-token` 헤더를 통해 요청을 검증합니다 +- **HTTPS 사용**: 프로덕션 환경에서는 반드시 HTTPS를 사용하세요 + +### 재시도 정책 + +- **자동 재시도**: ABC User Feedback은 웹훅 전송 실패 시 최대 3회까지 자동 재시도합니다 +- **재시도 간격**: 각 재시도는 3초 후에 실행됩니다 + +### 에러 처리 + +- **4xx 에러**: 클라이언트 오류로 간주하여 재시도하지 않습니다 +- **5xx 에러**: 서버 오류로 간주하여 재시도합니다 +- **네트워크 오류**: 연결 실패 시 재시도합니다 + +--- + +## 활용 사례 + +### 1. 자동 번역 + +```javascript +// FEEDBACK_CREATION 이벤트를 받아서 자동 번역 +if (event === 'FEEDBACK_CREATION') { + const translatedMessage = await translateText(data.feedback.message); + // 번역된 내용을 피드백에 업데이트 + await updateFeedback(data.feedback.id, { translatedMessage }); +} +``` + +### 2. 외부 티켓 시스템 연동 + +```javascript +// ISSUE_CREATION 이벤트를 받아서 외부 시스템에 티켓 생성 +if (event === 'ISSUE_CREATION') { + const ticketId = await createExternalTicket({ + title: data.issue.name, + description: data.issue.description, + priority: 'medium', + }); + // 외부 티켓 ID를 이슈에 저장 + await updateIssue(data.issue.id, { externalIssueId: ticketId }); +} +``` + +### 3. 알림 시스템 연동 + +```javascript +// ISSUE_STATUS_CHANGE 이벤트를 받아서 팀에 알림 +if (event === 'ISSUE_STATUS_CHANGE') { + await sendSlackNotification({ + channel: '#feedback-alerts', + message: `이슈 "${data.issue.name}"의 상태가 ${data.previousStatus}에서 ${data.issue.status}로 변경되었습니다.`, + }); +} +``` + +--- + +## 관련 문서 + +- [웹훅 관리](/docs/01-user-guide/07-settings/04-webhook-management.md) - UI에서 웹훅 설정하는 방법 +- [API 연동](./02-api-integration.md) - 웹훅과 함께 사용할 수 있는 API 활용 +- [이슈 관리](/docs/01-user-guide/05-issue-management.md) - 이슈 상태 변경 이벤트 이해 diff --git a/apps/docs/docs/02-developer-guide/_category_.json b/apps/docs/docs/02-developer-guide/_category_.json new file mode 100644 index 000000000..da7f033da --- /dev/null +++ b/apps/docs/docs/02-developer-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 3, + "label": "개발자 가이드" +} diff --git a/apps/docs/docs/02-developer-guide/index.md b/apps/docs/docs/02-developer-guide/index.md new file mode 100644 index 000000000..d02f62b88 --- /dev/null +++ b/apps/docs/docs/02-developer-guide/index.md @@ -0,0 +1,7 @@ +--- +title: 개발자 가이드 +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts new file mode 100644 index 000000000..e2d65fb04 --- /dev/null +++ b/apps/docs/docusaurus.config.ts @@ -0,0 +1,102 @@ +import type * as Preset from '@docusaurus/preset-classic'; +import type { Config } from '@docusaurus/types'; +import { themes as prismThemes } from 'prism-react-renderer'; + +import 'dotenv/config'; + +// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) + +const config: Config = { + title: 'ABC User Feedback', + tagline: + 'ABC User Feedback is a standalone web application designed to manage Voice of Customer (VoC) data', + favicon: 'img/logo.svg', + url: 'https://docs.abc-user-feedback.com', + // Set the // pathname under which your site is served + // For GitHub pages deployment, it is often '//' + baseUrl: '/', + + // Even if you don't use internationalization, you can use this field to set + // useful metadata like html lang. For example, if your site is Chinese, you + // may want to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'ko', + locales: ['ko', 'en', 'ja'], + localeConfigs: { + ko: { + label: '한국어', + direction: 'ltr', + }, + en: { + label: 'English', + direction: 'ltr', + }, + ja: { + label: '日本語', + direction: 'ltr', + }, + }, + }, + + presets: [ + [ + 'classic', + { + docs: { + routeBasePath: '/', + sidebarPath: './sidebars.ts', + editUrl: + 'https://github.com/line/abc-user-feedback/tree/dev/apps/docs', + }, + ...(process.env.GOOGLE_ANALYTICS_TRACKING_ID && { + gtag: { + trackingID: process.env.GOOGLE_ANALYTICS_TRACKING_ID, + anonymizeIP: true, + }, + }), + }, + ], + ], + themeConfig: { + // Replace with your project's social card + image: 'img/docusaurus-social-card.jpg', + navbar: { + title: 'ABC User Feedback', + logo: { alt: 'LOGO', src: 'img/logo.svg' }, + items: [ + { + type: 'docSidebar', + sidebarId: 'docs', + position: 'left', + label: 'Docs', + }, + { + type: 'localeDropdown', + position: 'right', + }, + { + href: 'https://github.com/line/abc-user-feedback', + label: 'GitHub', + position: 'right', + }, + ], + }, + ...(process.env.GOOGLE_SITE_VERIFICATION && { + metadata: [ + { + name: 'google-site-verification', + content: process.env.GOOGLE_SITE_VERIFICATION, + }, + ], + }), + footer: { + copyright: `Copyright © ${new Date().getFullYear()} ABC User Feedback.`, + }, + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + }, + } satisfies Preset.ThemeConfig, +}; + +export default config; diff --git a/apps/docs/eslint.config.mjs b/apps/docs/eslint.config.mjs new file mode 100644 index 000000000..67bd65d26 --- /dev/null +++ b/apps/docs/eslint.config.mjs @@ -0,0 +1,8 @@ +import baseConfig from '@ufb/eslint-config/base'; + +export default [ + { + ignores: ['dist/**', '**/*.js'], + }, + ...baseConfig, +]; diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current.json b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current.json new file mode 100644 index 000000000..32b8bc25b --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,27 @@ +{ + "version.label": { + "message": "Next", + "description": "The label for version current" + }, + "sidebar.docs.category.소개": { + "message": "Introduction", + "description": "The label for category Introduction in sidebar docs" + }, + "sidebar.docs.category.사용자 가이드": { + "message": "User Guide", + "description": "The label for category User Guide in sidebar docs" + }, + "sidebar.docs.category.설정": { + "message": "Settings", + "description": "The label for category Settings in sidebar docs" + }, + "sidebar.docs.category.개발자 가이드": { + "message": "Developer Guide", + "description": "The label for category Developer Guide in sidebar docs" + }, + "sidebar.docs.category.설치": { + "message": "Installation", + "description": "The label for category Installation in sidebar docs" + } +} + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/00-index.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/00-index.md new file mode 100644 index 000000000..66691b048 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/00-index.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 1 +slug: / +--- + +# Welcome + +Welcome! This documentation provides a comprehensive guide to ABC User Feedback. + +![ABC User Feedback](/assets/cover.png) + +## What is ABC User Feedback? + +ABC User Feedback is a standalone web application designed to efficiently collect, classify, and manage Voice of Customer (VoC) feedback. It provides various features including a feedback tagging system, kanban mode, issue tracker, SSO authentication, and more. It is currently being used in a service with 10 million MAU. + +

+

+ +## Key Features + +- **Feedback Tagging System**: Classify and manage feedback by topic +- **Kanban Mode**: Efficiently visualize and manage issue groups +- **Issue Tracker Integration**: Track issues with status indicators and integrate with external systems +- **Single Sign-On (SSO)**: OAuth authentication supporting enterprise-grade authentication requirements +- **Role-Based Access Control (RBAC)**: Granular user permission management +- **Dashboard**: Visualize statistical data for feedback and issues + +## Getting Started + +- [Installation Guide](/en/developer-guide/installation/docker-hub-images) - Installation methods using Docker, CLI tools, or manual setup +- [Tutorial](/en/user-guide/getting-started) - Basic usage guide + +## Get Support + +Have questions or need help? Check out these resources: + +- [GitHub Issues](https://github.com/line/abc-user-feedback/issues) - Bug reports and feature requests +- [GitHub Discussions](https://github.com/line/abc-user-feedback/discussions) - Community discussions + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/01-project-overview.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/01-project-overview.md new file mode 100644 index 000000000..e33a5dd53 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/01-project-overview.md @@ -0,0 +1,75 @@ +--- +sidebar_position: 1 +title: "Project Overview" +description: "Introducing the project overview." +--- + +# Project Overview + +## What is ABC User Feedback? + +ABC User Feedback is a standalone web application designed to efficiently collect, classify, and manage Voice of Customer (VoC). This open-source solution focuses on systematically managing user feedback to derive insights needed for product and service improvements. + +Currently, this application is being used in a service with 10 million Monthly Active Users (MAU), demonstrating proven stability for large-scale feedback processing. + +## Core Value Proposition + +ABC User Feedback provides the following core values: + +1. **Centralized Feedback Management**: Manage user feedback collected from various channels in one place +2. **Structured Analysis**: Classify feedback and identify trends through the issue system +3. **Issue Tracking**: Convert problems found in feedback into issues and track them +4. **Data-Driven Decision Making**: Visualize feedback data and derive insights through dashboards + +## Technology Stack + +ABC User Feedback is built on modern web technologies: + +- **Frontend**: [Next.js](https://nextjs.org/) - React-based frontend framework +- **Backend**: [NestJS](https://nestjs.com/) - Scalable backend framework based on TypeScript +- **Database**: [MySQL v8](https://www.mysql.com/) - Reliable relational database +- **Search Engine**: [OpenSearch v2.16](https://opensearch.org/) (Optional) - High-performance search functionality for large amounts of feedback data + +## Architecture Overview + +ABC User Feedback consists of the following main components: + +1. **Web Admin Interface**: Next.js-based web application providing user interfaces for feedback management, issue tracking, dashboards, etc. +2. **API Server**: NestJS-based backend server handling data processing, business logic, authentication, etc. +3. **Database**: MySQL database storing feedback, issues, user information, etc. +4. **Search Engine**: OpenSearch (optional) providing high-performance search for large amounts of feedback data +5. **SMTP Server**: Component responsible for sending emails required for user authentication processes such as email verification during account creation and password reset + +These components are containerized through Docker, making them easy to deploy and scale. + +## Main Use Cases + +ABC User Feedback is particularly useful in the following situations: + +1. **Product Improvement Process**: Collect and analyze user feedback to set product improvement directions +2. **Customer Support**: Efficiently track and manage user inquiries and issues +3. **User Experience Optimization**: Improve UX/UI based on user opinions +4. **Quality Management**: Systematically manage bug reports and feature requests +5. **Data-Driven Decision Making**: Support strategic decision-making using user feedback statistics + +## Differentiators + +ABC User Feedback differentiates itself from other feedback management tools with the following features: + +1. **Fully Open Source**: Unlike commercial solutions, it's completely free to use and customizable +2. **Enterprise-Grade Features**: Provides features needed for enterprise environments such as SSO authentication and RBAC +3. **Scalability**: Proven performance in large-scale user base (10 million MAU) +4. **Easy Integration**: Easy integration with existing systems through RESTful API and webhooks +5. **Containerization**: Easy deployment and scaling with Docker support + +## Next Steps + +To get started with ABC User Feedback, refer to the following documents: + +- [Key Features](./02-key-features.md) - Detailed feature descriptions +- [Installation Guide](/en/developer-guide/installation/docker-hub-images) - Installation methods + +--- + +This document provides a basic overview of ABC User Feedback. For more detailed information, refer to the documents in the relevant sections. + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/02-key-features.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/02-key-features.md new file mode 100644 index 000000000..7a13d9b93 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/02-key-features.md @@ -0,0 +1,173 @@ +--- +sidebar_position: 3 +title: 'Key Features' +description: 'Introducing key features.' +--- + +# Key Features + +ABC User Feedback provides various features to effectively collect, manage, and analyze user feedback. This document describes the core features in detail. + +## Feedback Tagging System + +![Feedback Tag](/assets/01-feedback-tag.png) + +The feedback tagging system is a core feature for systematically classifying and managing large amounts of user feedback. + +### Key Features + +- **Multiple Issue Assignment**: Assign multiple issues to each feedback for multidimensional classification +- **Custom Issue Creation**: Create and manage customized issues tailored to project characteristics +- **Issue-Based Filtering**: Filter feedback by issue to focus on specific topics +- **Issue Statistics**: Derive insights through analysis of issue usage frequency and trends + +### How to Use + +1. Create issue categories and issues in the admin panel +2. Assign relevant issues to received feedback +3. Filter and analyze feedback by issue +4. Identify key issues and trends through issue usage patterns + +## Kanban Mode + +![Issue Kanban](/assets/02-Issue-Kanban.png) + +Kanban mode is a feature for visually managing issue groups and optimizing workflows. + +### Key Features + +- **Intuitive Drag and Drop**: Simple interface for changing issue status +- **Status-Based Column Configuration**: Column separation based on issue progress status (e.g., To Do, In Progress, Done) +- **Workflow Visualization**: Understand team work processes and progress at a glance +- **Workload Management**: Monitor workload through the number of issues in each status +- **Filter and Sort**: Filter and sort issues in the kanban board by various criteria + +### How to Use + +1. Select kanban mode view +2. Check and manage issues by status +3. Change issue status with drag and drop +4. Optimize team workflows and identify bottlenecks + +## Issue Tracker Integration + +![Issue Tracker](/assets/03-issue-tracker.png) + +Issue tracker integration is a feature for systematically managing problems or improvements found in feedback. + +### Key Features + +- **Status Indicators**: Visually display the current status of issues (New, In Progress, Resolved, etc.) +- **External System Integration**: Connect with issue tracker systems (JIRA) + +### How to Use + +1. Create issues from feedback or in the issue menu +2. Configure external issue tracker connection (optional) +3. Set issue details and issue tracking ticket +4. Monitor and update issue progress +5. Close issue after resolution + +## Single Sign-On (SSO) + +![Single Sign-on](/assets/04-single-signon.png) + +Single Sign-On simplifies authentication processes in enterprise environments and enhances security. + +### Key Features + +- **OAuth Support**: Authentication support through various OAuth providers +- **Enterprise ID Integration**: Seamless integration with existing enterprise ID systems +- **Centralized User Management**: Manage user access through a single authentication system +- **Enhanced Security**: Apply multi-factor authentication and enterprise security policies +- **Simplified Login Experience**: No need for users to create additional accounts + +### Supported SSO Providers + +- Google +- Custom (Standard OAuth 2.0 and OpenID Connect providers) + +### How to Use + +1. Configure SSO provider in admin settings +2. Set authentication parameters and redirect URLs +3. Configure user attribute mapping +4. Enable and test SSO login + +## Role-Based Access Control (RBAC) + +![Role Management](/assets/05-role-management.png) + +Role-Based Access Control is a feature for effectively managing user permissions and maintaining system security. + +### Key Features + +- **Predefined Roles**: Provides basic roles such as Administrator, Analyst, Viewer +- **Custom Role Creation**: Create customized roles and permissions tailored to organizational structure +- **Granular Permission Control**: Set access permissions by function and data +- **Role Assignment Management**: Assign and change roles per user +- **Permission Inheritance**: Support hierarchical permission structures + +### How to Use + +1. Access role management menu in admin panel +2. Create new roles or modify existing roles as needed +3. Assign appropriate roles to users +4. Regularly review permissions and access scope by role + +## Dashboard + +![Dashboard](/assets/06-dashboard.png) + +The dashboard is a feature that visualizes feedback data to understand important insights at a glance. + +### Key Features + +- **Real-time Statistics**: Real-time display of key metrics such as feedback count, issue count, resolution rate +- **Trend Analysis**: Graphs showing feedback and issue trends over time +- **Issue Distribution**: Visualization of feedback distribution by issue + +### Provided Charts and Widgets + +1. **Feedback Summary Cards**: Key metrics such as total feedback count, new feedback, processed feedback +2. **Time Series Graphs**: Daily/weekly/monthly feedback trends +3. **Issue Status Donut Chart**: Distribution by issue status + +### How to Use + +1. Access the dashboard page +2. Adjust data range through period and filter settings +3. Analyze key metrics and trends +4. Derive insights-based decisions and action items + +## Additional Features + +In addition to the key features described above, ABC User Feedback provides the following additional features: + +### API Integration + +- Integration with external systems through RESTful API +- Programmatic feedback collection and management + +### Webhooks + +- Notify external systems when major events occur +- Support building automated workflows + +### Image Storage Integration + +- Manage user-submitted images through S3-compatible storage +- Attach screenshots and images to feedback + +### Data Export + +- Export feedback data in CSV, Excel formats + +### Multi-Language Support + +- Provide interfaces in various languages +- Multi-language feedback management for international teams + +--- + +This document provides an overview of ABC User Feedback's key features. For more detailed usage of each feature, refer to the [User Guide](/en/user-guide/getting-started) section. diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/_category_.json b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/_category_.json new file mode 100644 index 000000000..9233aebc2 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/00-introduction/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 1, + "label": "Introduction" +} + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/01-getting-started.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/01-getting-started.md new file mode 100644 index 000000000..90b6c3755 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/01-getting-started.md @@ -0,0 +1,263 @@ +--- +title: Getting Started +description: This document explains how to start the system from initial setup to collecting the first feedback after installing ABC User Feedback. +sidebar_position: 1 +--- + +# Getting Started + +After installing ABC User Feedback for the first time, initial setup is required to use the system. This document provides step-by-step guidance from tenant creation to collecting the first feedback. + +--- + +## Initial Setup Overview + +To start ABC User Feedback, proceed with the following setup steps in order: + +1. **Create tenant and administrator account** +2. **First login and profile setup** +3. **Create project** +4. **Create channel and configure fields** +5. **Generate API key** +6. **Test first feedback collection** + +--- + +## Accessing the System + +If you need to install ABC User Feedback, first proceed with [Installation using Docker Hub images](/en/developer-guide/installation/docker-hub-images). + +After completing the installation, access ABC User Feedback through a web browser: + +``` +http://localhost:3000 +``` + +> If you changed the port or domain, enter the address according to your settings. + +--- + +## Creating Tenant and Administrator Account + +![member-register.png](/img/tenant.png) + +When you first access the system, the **Tenant Creation and Administrator Account Registration** screen will be displayed. + +### Step 1: Enter Tenant Information + +Set the tenant name. + +After entering the tenant name, click the **Next** button. + +> This tenant name will be displayed in the login UI. + +### Step 2: Create Administrator Account + +Create the first administrator account for the system. + +1. Enter the administrator account email and click the **Request Code** button. +2. Check the authentication code in your email inbox and enter it +3. Click the **Verify** button +4. After verification is complete, set a password. + +:::info Password Requirements + +- **At least 8 characters** +- **Include letters** (A–Z, a–z) +- **Include special characters** (e.g., `@`, `#`, `!`) +- **No consecutive characters** (e.g., `aa`, `11`) + +> **Examples**: ✅ `MyCompany2024!`, ❌ `12345678`, `password` + +::: + +After tenant and administrator account creation is complete, a confirmation screen will be displayed. + +**Next step**: Click the **Confirm** button to proceed to the login screen. + +--- + +## Logging In + +Log in for the first time with the created administrator account. + +1. **Email**: Enter the administrator email registered earlier +2. **Password**: Enter the set password +3. Click the **Sign In** button + +--- + +## Creating First Project + +After logging in, the project creation wizard will automatically start. + +### Understanding System Structure + +ABC User Feedback has the following hierarchical structure: + +``` +Tenant (Organization) + └── Project (Product/Service Unit) + └── Channel (Feedback Collection Path) +``` + +### Step 1: Project Basic Information + +| Item | Description | Example | +| --------------- | ------------------------------------------------- | ------------------------------------------- | +| **Name** | Project name | `Mobile App`, `Web Service` | +| **Description** | Project description (optional) | `Customer feedback collection and analysis` | +| **Time Zone** | Time reference (affects dashboard and statistics) | `Asia/Seoul` | + +**After completion**: After entering the information, click the **Next** button. + +### Step 2: Invite Team Members (Optional) + +In this step, you can invite team members to the project. You can skip this now and add them later anytime. + +### Step 3: Generate API Key (Optional) + +You can pre-generate an API key for integration with external systems. + +### Project Creation Complete + +After entering all information, project creation will be complete. + +**Select next step**: + +- **Create Channel**: Immediately create a channel to start collecting feedback +- **Skip for Now**: Create channel later + +--- + +## Creating First Channel + +After project creation, you need to create a **channel** to actually collect feedback. + +### Understanding Channel Concept + +A channel represents a **feedback collection path**: + +- Website inquiry form +- In-app feedback in mobile app +- Customer service VoC +- Survey responses + +### Step 1: Channel Basic Information + +| Item | Description | Example | +| ---------------------------------- | -------------------------------------------------------- | ---------------------------- | +| **Name** | Channel name | `Web Feedback`, `App Review` | +| **Description** | Channel description (optional) | `Website user opinions` | +| **Maximum Feedback Search Period** | Searchable period for feedback (30/90/180/365 days, all) | `90 days` | + +**After completion**: After entering the information, click the **Next** button. + +### Step 2: Field Configuration + +Define the data structure to collect in the channel. + +#### Default Fields + +Fields automatically created by the system: + +| Field Name | Format | Property | Description | +| ----------- | ----------- | --------- | --------------------- | +| `id` | number | Read Only | Unique feedback ID | +| `createdAt` | date | Read Only | Creation time | +| `updatedAt` | date | Read Only | Modification time | +| `issues` | multiSelect | Editable | List of linked issues | + +#### Adding Custom Fields + +Add custom fields for actual feedback collection: + +1. Click the **Add Field** button +2. Enter field information: + +| Item | Description | Example | +| ---------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------- | +| **Key** | Unique identifier (uppercase/lowercase letters, numbers, `_`) | `message`, `rating` | +| **Display Name** | Name displayed in UI | `Feedback Content` | +| **Format** | Data format | `text`,`keyword`,`number`,`date`,`select`,`multiSelect`,`images`,`aiField` | +| **Property** | `Editable` (modifiable in UI) / `Read Only` (not modifiable) | `Editable` | +| **Status** | `Active` / `Inactive` | `Active` | + +#### Recommended Default Field Configuration + +For the first channel, it is recommended to add the following fields: + +| Key | Display Name | Format | Description | +| ----------- | ---------------- | ------- | ------------------ | +| `message` | Feedback Content | text | User feedback | +| `userEmail` | User Email | keyword | Contact (optional) | +| `rating` | Satisfaction | number | 1-5 point rating | + +### Field Preview + +After completing field configuration, you can preview the feedback input screen with the **Preview** button. + +**After completion**: Complete channel creation with the **Complete** button. + +### Channel Creation Complete + +**Next step**: Click the **Start** button to begin collecting feedback. + +--- + +## Testing First Feedback Collection + +After channel creation is complete, you can actually collect feedback. + +### Registering Feedback via API + +Let's register the first feedback using the created API key. + +#### API Request Example + +```bash +curl -X POST http://localhost:4000/api/projects/1/channels/1/feedbacks \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: YOUR_API_KEY" \ + -d '{ + "message": "The app runs slowly", + "userEmail": "user@example.com", + "rating": 3 + }' +``` + +> Replace `YOUR_API_KEY` with the actual API key created earlier. + +#### Checking Success Response + +If the API request succeeds, you will receive the following response: + +```json +{ + "id": 1 +} +``` + +### Checking Feedback + +Let's check the registered feedback in the web interface. + +1. Click the **Feedback** tab in the top menu +2. Check the registered feedback in the feedback list +3. Click the feedback to view detailed information + +### Creating First Issue + +Let's create an issue based on the feedback. + +1. In the feedback detail screen, click the **`+` button** in the **Issue** section +2. Enter the issue name and press **Enter** or click the **Create** button +3. Check the created issue + +## Next Steps Guide + +Basic setup and first feedback collection are complete! + +## Related Documents + +- [API Integration](/en/developer-guide/api-integration) - Detailed API usage guide diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/02-project-management.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/02-project-management.md new file mode 100644 index 000000000..7c8932921 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/02-project-management.md @@ -0,0 +1,347 @@ +--- +title: Project +description: This document explains how to create, configure, and manage projects in ABC User Feedback, and how to set team member roles and permissions. +sidebar_position: 2 +--- + +# Project + +In ABC User Feedback, a **project** is the most basic unit for collecting and analyzing feedback. This document covers features from project creation to team management and permission settings. + +--- + +## Project Overview + +Projects have the following hierarchical structure: + +``` +Tenant + └── Project (multiple allowed) + ├── Channel (multiple allowed) + ├── Members and Roles + ├── Issue Tracker Integration + ├── Webhook Integration + ├── AI Features + └── API Keys +``` + +Each project is a management unit that includes multiple channels, and can independently set and operate team members, roles, issue tracker integration, external system integration, etc. + +--- + +## Creating a Project + +### Access Permissions + +Only **Super users** can create projects. Regular users can participate by being invited as members to existing projects. + +> If you need Super user permissions, please contact your system administrator. + +### Access Method + +There are two ways to create a new project: + +1. **First login**: The project creation wizard automatically starts +2. **Additional projects**: Click the **Create Project** button at the top of the left sidebar + +### Step 1: Project Basic Information + +![create-project-1](/img/project/1.png) + +When creating a project, enter the following information: + +| Item | Description | Example | +| --------------- | -------------------------------------------------------------- | ------------------------------------------------ | +| **Name** | Project name (required) | `Mobile App`, `Customer Service`, `Beta Service` | +| **Description** | Brief description (optional) | `iOS/Android app user feedback collection` | +| **Time Zone** | Used as the time reference for feedback and reports (required) | `Asia/Seoul` | + +> The time zone affects **dashboard statistics**. + +**After completion**: After entering all information, click the **Next** button. + +### Step 2: Add Team Members (Optional) + +![create-project-2](/img/project/2.png) + +This step can be **skipped**. You can add members later from the project settings at any time. + +#### Adding Members + +1. Click the **Register Member** button at the top right +2. Enter the following items: + - **Email**: Select a user registered in the system + - **Role**: Choose from Admin, Editor, or Viewer + +> If you want to use custom roles, click the **Role Management** button for additional settings. + +**After completion**: Check the member list and click **Next**. + +### Step 3: Generate API Key (Optional) + +![create-project-3](/img/project/3.png) + +API keys are used when collecting feedback from external systems. You can generate them later from the settings menu, so you can skip this step now. + +#### Key Generation Method + +1. Click the **Create API Key** button at the top right +2. The key is automatically generated and displayed in the list +3. Copy the generated key and store it in a safe place + +### Project Creation Complete + +![create-project-4](/img/project/4.png) + +After completing all steps, a **summary screen** appears: + +- Project information: name, description, time zone +- Member list +- Generated API keys +- Role settings status + +#### Next Steps + +- To immediately create a channel and start collecting feedback, click the **Create Channel** button +- Or click the **Later** button to create it later + +--- + +## Managing Project Settings + +![project-setting.png](/img/project/project-setting.png) + +### Access Method + +To change project settings: + +1. Click **Settings** in the top menu +2. Select **Project Setting** from the left menu + +### Editing Basic Information + +You can modify the following items at any time: + +| Item | Description | Notes | +| --------------- | --------------------------------------------------- | ----------------------------------- | +| **Name** | Project name | Name displayed to team members | +| **Description** | Description (optional) | Purpose of the project | +| **Time Zone** | Time reference for statistics and time-related data | Changes do not affect existing data | + +**Save method**: After making changes, click the **Save** button at the top right. + +### Notes on Time Zone Changes + +- Does not affect the time information of existing feedback/issues +- Data inconsistencies may occur in dashboard statistics after changes. + +### Deleting a Project + +#### Deletion Procedure + +To completely delete a project: + +1. Click the **Delete Project** button at the bottom of the Project Setting screen +2. Enter the project name exactly in the confirmation popup +3. Click the **Delete** button to finalize + +#### Deletion Notes + +- **All feedback, issues, and settings within the project will be permanently deleted** +- **Cannot be undone**, so backup or export is recommended beforehand +- Connected channels and API keys are also removed upon deletion + +--- + +## Member Management + +![member-setting.png](/img/project/member-setting.png) + +### Viewing Member List + +To view members currently participating in the project: + +1. Click **Settings** in the top menu +2. Select **Member Management** from the left menu + +The member list displays the following information: + +| Item | Description | +| ---------- | -------------------------- | +| Email | Account email | +| Name | User name (from profile) | +| Department | Department | +| Role | Role within the project | +| Joined | Project participation date | + +### Inviting New + +![member-register.png](/img/project/member-register.png) + +#### Invitation Procedure + +1. Click the **Register Member** button +2. Enter invitation information: + +| Item | Description | +| --------- | -------------------------------------------- | +| **Email** | Email of the user to invite | +| **Role** | Role to assign (Admin, Editor, Viewer, etc.) | + +3. Click the **Invite** button to complete the invitation + +### Editing Member Information + +To modify existing member information: + +1. Click the row of the member you want to edit in the member list +2. In the popup, you can modify the Role: +3. Click the **Save** button to save changes + +### Removing Members + +To remove a member from the project: + +1. Click the **Delete** button at the bottom of the member edit popup +2. Click **Confirm** in the confirmation message + +> Removing a member does not delete feedback/issue records created by that user; only project access permissions are removed. + +--- + +## Role and Permission Management + +![role-setting.png](/img/project/role-setting.png) + +### Default Roles + +The system provides the following default roles: + +| Role | Permission Summary | +| ---------- | ---------------------------------------------------------------------- | +| **Admin** | Access to all features. Includes project deletion | +| **Editor** | Can create, modify, and delete feedback/issues. Cannot access settings | +| **Viewer** | View only. Cannot modify, delete, or access settings | + +### Creating Custom Roles + +![role-create.png](/img/project/role-create.png) + +You can create custom roles when more granular permissions are needed: + +1. Click the **Role Management** link in the Member Management screen +2. Click the **Create Role** button +3. Enter the role name and permissions: + +### Permission Settings + +For each role, you can set the following feature-specific permissions: + +#### Feedback Permissions + +| Permission Item | Description | +| ----------------------------------- | -------------------------------- | +| **Download Feedback** | Download feedback data | +| **Edit Feedback** | Edit feedback | +| **Delete Feedback** | Delete feedback | +| **Attach/Detach Issue in Feedback** | Link/unlink issues with feedback | + +#### Issue Permissions + +| Permission Item | Description | +| ---------------- | ------------- | +| **Create Issue** | Create issues | +| **Edit Issue** | Edit issues | +| **Delete Issue** | Delete issues | + +#### Project Management + +| Permission Item | Description | +| --------------------- | ------------------------ | +| **Edit Project Info** | Edit project information | +| **Delete Project** | Delete project | + +#### Member Management + +| Permission Item | Description | +| ------------------------- | ---------------------- | +| **Read Project Member** | View project members | +| **Create Project Member** | Invite project members | +| **Edit Project Member** | Edit project members | +| **Delete Project Member** | Remove project members | + +#### Role Management + +| Permission Item | Description | +| ----------------------- | -------------------- | +| **Read Project Role** | View project roles | +| **Create Project Role** | Create project roles | +| **Edit Project Role** | Edit project roles | +| **Delete Project Role** | Delete project roles | + +#### API Key Management + +| Permission Item | Description | +| ------------------ | --------------- | +| **Read API Key** | View API keys | +| **Create API Key** | Create API keys | +| **Edit API Key** | Edit API keys | +| **Delete API Key** | Delete API keys | + +#### Issue Tracker + +| Permission Item | Description | +| ---------------------- | ----------------------- | +| **Read Issue Tracker** | View issue tracker | +| **Edit Issue Tracker** | Configure issue tracker | + +#### Webhook Management + +| Permission Item | Description | +| ------------------ | --------------- | +| **Read Webhook** | View webhooks | +| **Create Webhook** | Create webhooks | +| **Edit Webhook** | Edit webhooks | +| **Delete Webhook** | Delete webhooks | + +#### AI and Channel Settings + +| Permission Item | Description | +| ---------------------- | ---------------- | +| **Read Generative AI** | View AI settings | +| **Edit Generative AI** | Edit AI settings | + +#### Channel-Related Settings + +| Permission Item | Description | +| ---------------------- | ------------------------ | +| **Edit Channel Info** | Edit channel information | +| **Delete Channel** | Delete channel | +| **Read Field** | View fields | +| **Edit Field** | Edit fields | +| **Read Image Setting** | View image settings | +| **Edit Image Setting** | Edit image settings | +| **Create Channel** | Create new channels | + +### Permission Setting Tips + +#### Security Best Practices + +- **Principle of least privilege**: Grant only the minimum permissions necessary for work +- **Regular review**: Check permissions when team changes or employees leave +- **Limit Admin role**: Keep the number of administrators as small as possible + +### Editing and Deleting Roles + +- **Edit**: Click the desired item in the role list to modify the name and permissions +- **Delete**: Roles not in use can be deleted with the **Delete** button + +> **Note**: At least one Admin role must always exist and cannot be deleted. + +--- + +## Related Documents + +- [Channel Management](./03-channel-management.md) - Channel creation and field settings +- [Feedback Management](./04-feedback-management.md) - Feedback collection and analysis +- [API Integration](/en/developer-guide/api-integration) - API key usage diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/03-channel-management.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/03-channel-management.md new file mode 100644 index 000000000..6acae26aa --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/03-channel-management.md @@ -0,0 +1,237 @@ +--- +title: Channel +description: This document explains how to create, configure, and manage feedback collection channels in ABC User Feedback, and how to handle custom fields and image settings. +sidebar_position: 3 +--- + +# Channel + +A **Channel** is a unit that distinguishes feedback collection paths or purposes. Each channel has an independent field structure, image settings, and AI features, allowing configuration for various feedback collection scenarios. + +--- + +## Channel Overview + +### Role of Channels + +Channels serve the following roles: + +- **Distinguish feedback collection paths**: Web, app, customer service, surveys, etc. +- **Define data structure**: Unique field settings per channel +- **Manage collection policies**: Image allowance, search period, security settings, etc. +- **Provide analysis units**: Independent statistics and analysis per channel + +--- + +## Creating a Channel + +### Access Method + +To create a new channel: + +1. **Right after project creation**: Click the **Create Channel** button on the project completion screen +2. **Additional channel creation**: Click the **Create Channel** button in **Settings > Channel List** + +### Step 1: Channel Basic Information + +![channel-create-1](/img/channel/1.png) + +| Item | Description | Example | +| ---------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------ | +| **Name** | Channel name (required) | `Web Feedback`, `App Review`, `Customer Service` | +| **Description** | Brief channel description (optional) | `Website user opinion collection` | +| **Maximum Feedback Search Period** | Maximum searchable period for feedback (30/90/180/365 days, all) | `90 days` | + +#### Notes on Maximum Feedback Search Period Setting + +- **Impact scope**: Directly affects the feedback download functionality +- **Download behavior**: All feedback within the set search period becomes the download target +- **Performance testing**: If daily feedback count is high, test with various periods to find the optimal value +- **Gradual adjustment**: Start with a short period and gradually increase as needed + +**After completion**: After entering the information, click the **Next** button. + +### Step 2: Field Configuration + +![channel-create-2](/img/channel/2.png) + +Define the data structure to collect in the channel. This directly affects the API request structure and feedback table configuration. + +#### Default System Fields + +Fields automatically included in all channels: + +| Key | Format | Property | Description | +| ----------- | ----------- | --------- | -------------------------- | +| `id` | number | Read Only | Unique feedback ID | +| `createdAt` | date | Read Only | Feedback creation time | +| `updatedAt` | date | Read Only | Feedback modification time | +| `issues` | multiSelect | Editable | List of linked issues | + +> These fields cannot be deleted or have their main properties modified. + +#### Adding Custom Fields + +Add fields that match actual business requirements. + +1. Click the **Add Field** button +2. Enter field information + +| Item | Description | Example | +| ---------------- | ------------------------------------------------------------- | -------------------------------- | +| **Key** | Unique identifier (uppercase/lowercase letters, numbers, `_`) | `message`, `rating` | +| **Display Name** | Name displayed in UI | `Feedback Content`, `User Email` | +| **Format** | Data format (see table below) | `text`, `keyword`, `number` | +| **Property** | `Editable` (can input) / `Read Only` (view only) | `Editable` | +| **Status** | `Active` / `Inactive` | `Active` | +| **Description** | Easy-to-understand description for team members (optional) | `User-entered feedback content` | + +### Field Format Types + +| Format | Description | Usage Example | API Example | +| ------------- | ------------------------ | ----------------------------------------------- | ------------------------ | +| `text` | Free text input | Feedback content, detailed description | `"App keeps freezing"` | +| `keyword` | Short keyword/tag | Version info, page name | `"v1.2.3"` | +| `number` | Number | Rating, age, usage time | `5` | +| `date` | Date | Occurrence date, expiration date | `"2024-03-01T00:00:00Z"` | +| `select` | Single selection | Category, priority | `"Feature Request"` | +| `multiSelect` | Multiple selection | Tags, related features | `["Bug", "UI"]` | +| `images` | Image URL array | Screenshots, attachments | `["https://..."]` | +| `aiField` | AI analysis result field | Sentiment analysis, summary, keyword extraction | `"Positive"` | + +> **About images format**: For detailed image setting methods, refer to the [Image Settings](/en/user-guide/settings/image-setting) document. +> +> **About aiField format**: For AI field settings and template configuration methods, refer to the [AI Settings](/en/user-guide/settings/ai-setting) document. + +### Field Configuration Examples + +#### Web Feedback Channel + +| Key | Display Name | Format | Purpose | +| ------------- | ---------------- | ------- | ------------------------------- | +| `message` | Feedback Content | text | User opinion | +| `userEmail` | Email | keyword | Contact (optional) | +| `pageUrl` | Page URL | keyword | Feedback occurrence location | +| `category` | Category | select | Bug/Feature Request/Improvement | +| `priority` | Priority | select | High/Medium/Low | +| `screenshots` | Screenshots | images | Problem situation capture | + +#### Mobile App Review Channel + +| Key | Display Name | Format | Purpose | +| ------------ | -------------- | ------- | -------------------- | +| `message` | Review Content | text | User review | +| `rating` | Rating | number | 1-5 point rating | +| `appVersion` | App Version | keyword | For bug tracking | +| `deviceType` | Device Type | select | iOS/Android | +| `crashLogs` | Crash Logs | text | Technical error info | + +### Field Preview + +After completing field configuration, you can preview the actual feedback input screen with the **Preview** button. + +This preview matches the field structure required for API requests. + +**After completion**: Proceed to the next step with the **Next** button. + +### Step 3: Channel Creation Complete + +![create-channel-3](/img/channel/3.png) + +Once all steps are completed, a **summary screen** appears: + +- Channel information: Name, description, time zone +- Field information + +--- + +## Field Management + +![field-management.png](/img/channel/field-management.png) + +### Editing Fields + +To modify existing fields, click the row of the field you want to edit in the field list and modify the information. + +> **Note**: `Key` and `Format` cannot be modified after creation. This is restricted for data consistency. + +### Field Deletion + +To ensure data integrity and consistency, **field deletion is not provided**. + +#### Recommended Method Instead of Deletion + +1. **Change to Inactive status**: Disable the field to exclude it from new feedback collection +2. **Preserve data**: Keep existing collected feedback data as is +3. **Use filtering**: Display only Active fields in the field list for management efficiency + +#### When Complete Removal is Needed + +If you need to completely remove a field: + +- Consider deleting the entire channel and creating a new one +- Export data and migrate to a new structure +- Consult with the development team for database-level processing + +### Field Status Management + +#### Active / Inactive Toggle + +- **Active**: Fields used during feedback collection +- **Inactive**: Temporarily disabled fields (data is preserved) + +#### Filtering Options + +You can filter fields by the following conditions using the top controls: + +- **Status**: `Active` / `Inactive` +- **Property**: `Editable` / `Read Only` + +--- + +## Channel Information Management + +![channel-setting](/img/channel/channel-setting.png) + +### Editing Channel Basic Information + +You can modify the basic information of created channels. + +#### Access Method + +1. **Settings > Channel List > [Select Channel]** +2. Click the **Channel Information** tab + +#### Editable Items + +| Item | Editable | Notes | +| ---------------------------------- | -------- | ------------------------------ | +| **Channel ID** | ❌ No | Internal system identifier | +| **Channel Name** | ✅ Yes | Name displayed to team members | +| **Description** | ✅ Yes | Channel purpose | +| **Maximum Feedback Search Period** | ✅ Yes | May affect performance | + +### Channel Deletion + +You can delete channels that are no longer in use. + +#### Deletion Procedure + +1. Click the **Delete Channel** button at the bottom of the Channel Information screen +2. Enter the channel name exactly in the confirmation popup +3. Finalize with the **Delete** button + +#### Deletion Notes + +- **All feedback data for that channel will be permanently deleted** +- **Cannot be undone**, so backup or export is recommended beforehand +- Related API key settings should also be checked + +--- + +## Related Documents + +- [Project Management](./02-project-management.md) - Project settings and permission management +- [Feedback Management](./04-feedback-management.md) - Analysis and utilization of collected feedback +- [API Integration](/en/developer-guide/api-integration) - Integration methods with external systems +- [AI Integration](/en/user-guide/settings/ai-setting) - AI feature settings diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/04-feedback-management.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/04-feedback-management.md new file mode 100644 index 000000000..8936dc424 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/04-feedback-management.md @@ -0,0 +1,336 @@ +--- +title: Feedback +description: This document explains how to create, view, analyze, and manage feedback in ABC User Feedback. +sidebar_position: 4 +--- + +# Feedback + +Feedback is the core data of ABC User Feedback. This document covers all features related to feedback, from creation to analysis and management. + +![feedback](/img/feedback/0.png) + +--- + +## Creating Feedback + +Feedback is mainly created by external systems (websites, mobile apps, API integration), but administrators can also create it directly. + +### Creating Feedback via API + +This is the most common method of creating feedback. + +#### Basic API Request Structure + +```bash +curl -X POST http://your-domain.com/api/v1/projects/{projectId}/channels/{channelId}/feedbacks \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: YOUR_API_KEY" \ + -d '{ + "message": "User feedback content", + "userEmail": "user@example.com", + "category": "Bug Report" + }' +``` + +#### Request Examples by Channel Fields + +The request structure varies depending on each channel's field settings: + +**Web Feedback Channel Example**: + +```json +{ + "message": "Login button is not working", + "userEmail": "user@company.com", + "pageUrl": "https://example.com/login", + "category": "Bug", + "priority": "High", + "browserInfo": "Chrome 119.0.0" +} +``` + +**Mobile App Channel Example**: + +```json +{ + "message": "App freezes frequently", + "rating": 2, + "appVersion": "v2.1.3", + "deviceType": "iOS", + "crashLogs": "Exception in thread main..." +} +``` + +#### Feedback with Images + +When using image URL method: + +```json +{ + "message": "Screen appears broken", + "userEmail": "user@example.com", + "images": [ + "https://cdn.example.com/screenshot1.png", + "https://cdn.example.com/screenshot2.png" + ] +} +``` + +### Verifying Feedback Creation + +Created feedback is immediately displayed in the feedback list. + +--- + +## Accessing Feedback List + +Access the feedback list to view and manage created feedback. + +### Access Method + +1. Select the desired **project** from the left sidebar +2. Click the desired **channel** from the channel list at the bottom +3. Select the **Feedback** tab in the top menu + +### Feedback Table Structure + +The feedback list is displayed in table format with the following basic structure: + +| Column Type | Description | Example | +| ------------------- | --------------------------------------------- | ---------------------------- | +| **Default Columns** | Displayed for all channels | ID, Created, Updated, Issue | +| **Custom Columns** | Displayed according to channel field settings | Message, UserEmail, Category | + +--- + +## Feedback Filtering/Sorting/View Options + +Various tools are provided to quickly find desired information from large amounts of feedback data. + +![feedback-option](/img/feedback/1.png) + +### Date Filtering + +You can set the viewing period with the **Date** button at the top. + +#### Available Period Options + +| Option | Description | Use Case | +| ---------------- | --------------------------- | ------------------------ | +| **Today** | Feedback registered today | Real-time monitoring | +| **Yesterday** | Previous day's feedback | Daily review | +| **Last 7 Days** | Recent 1 week of data | Weekly analysis | +| **Last 30 Days** | Recent 1 month of data | Monthly trend analysis | +| **Custom** | Set start-end date directly | Specific period analysis | + +### Filters + +Click the **Filter** button to filter feedback by various conditions. + +![feedback-filter](/img/feedback/2.png) + +#### Filter Structure + +``` +Where: First condition +And: Feedback that satisfies all conditions +Or: Feedback that satisfies at least one condition +``` + +> **Note**: `And` and `Or` cannot be mixed at the same time. + +#### Filter Options by Field Type + +| Field Type | Available Operators | Example | +| --------------- | ------------------------------------------ | ----------------------------- | +| **text** | Contains (partial match) | message contains "bug" | +| **keyword** | Is (exact match) | category is "Feature Request" | +| **number** | Is (exact match) | rating == 3 | +| **select** | Is (exact match) | | +| **multiSelect** | Is (exact match), Contains (partial match) | | +| **aiField** | Contains (partial match) | | +| **date** | Is (exact match), Between (period match) | created between date range | + +#### Filter Usage Examples + +**Advanced Search for Multi-Select Category**: + +``` +Where: category contains "Bug" +And: priority is "High" +``` + +**Finding Feedback Linked to Multiple Issues**: + +``` +Where: issues contains "Login Issue" +Or: issues contains "UI Improvement" +``` + +**Finding 4-Rating Feedback in Specific Category**: + +``` +Where: category is "Feature Request" +And: rating is 4 +``` + +### Sorting Function + +Click table headers to sort by that column. This feature is available for Created and Updated columns. + +### View Options + +You can adjust the display method of the feedback list to suit your needs. + +#### Expand Feature + +Click the **Expand** button to preview detailed content of each feedback in the table. + +**Usage**: + +- Check main content without opening detail panel +- Quickly browse through multiple feedback +- View full content of long text fields + +#### Show/Hide Columns + +You can select columns to display with the **View** button at the top of the table. + +**Features**: + +- **Required columns**: ID, Created are always displayed (cannot be hidden) +- **Optional columns**: Custom fields can be individually shown/hidden +- **Screen optimization**: Efficiently use screen space by displaying only needed information + +**Usage Tips**: + +``` +For monitoring: Display only ID, Created, Message +For analysis: Display all custom fields +For review: Display Message, Category, Priority +``` + +## Viewing/Editing/Deleting Feedback + +You can view detailed information of individual feedback and edit or delete it as needed. + +### Viewing Feedback Details + +#### Access Method + +Click a **row** in the feedback table to open the detail view panel on the right. + +![feedback-detail](/img/feedback/3.png) + +### Detail Panel Structure + +The detail panel is structured as follows: + +#### 1. Basic Information Area + +- **Feedback ID**: Unique identification number +- **Creation Time**: Initial registration date/time +- **Modification Time**: Last change date/time +- **Issues**: Tagged issues + +#### 2. Custom Fields Area + +All custom fields set in the channel are displayed. + +### Editing Feedback + +#### Editable Fields + +Click the **Edit** button in the detail panel to switch to edit mode. + +#### Editable Items + +| Item | Editable | Notes | +| ------------------ | -------- | -------------------------------------------------------------------- | +| **Default Fields** | ❌ No | ID, creation date, etc. | +| **Custom Fields** | ✅ Yes | Varies by field setting Property, not possible if Status is Inactive | + +#### Completing Edits + +1. Modify necessary information +2. Click the **Save** button +3. Changes are immediately reflected and "Updated" time is refreshed + +### Issue Link Management + +#### Creating New Issue + +1. Click the **+ button** in the issue column +2. Enter the issue name and click the **Create** option + +#### Linking Existing Issue + +1. Click the **+ button** in the issue section +2. Enter the name of the issue to link +3. Select the issue to link from the dropdown + +#### Unlinking Issue + +1. Click the **+ button** in the issue section +2. Select the issue to unlink + +### Deleting Feedback + +#### Single Feedback Deletion + +1. Click the **Delete Feedback** button at the bottom of the detail panel +2. Approve deletion in the confirmation dialog + +#### Multiple Feedback Deletion + +1. Select multiple feedback using **checkboxes** in the feedback list +2. Click the **Delete Selected** button that appears at the top +3. Confirm batch deletion + +#### Deletion Notes + +- **Cannot be recovered**: Deleted feedback cannot be restored +- **Issue links removed**: Linked issues remain but links are removed +- **Statistics impact**: Data is excluded from dashboard statistics + +--- + +## Downloading Feedback + +You can export collected feedback data in various formats for analysis or backup. + +### Accessing Download Function + +#### Download All Feedback + +1. Click the **Export** button at the top of the feedback list + +#### Download Filtered Feedback + +1. Apply filtering with desired conditions +2. Click the **Export** button to download only data matching current filter conditions + +#### Download Selected Feedback + +1. Select specific feedback using checkboxes +2. Click the **Export Selected** button + +### Download Format Selection + +When clicking the Export button, you can select the download format. + +#### Supported Formats + +| Format | Extension | Advantages | Recommended Use Cases | +| --------- | --------- | ---------------------------------------- | ---------------------------------- | +| **CSV** | `.csv` | Lightweight and highly compatible | Excel, Google Sheets analysis | +| **Excel** | `.xlsx` | Format preservation, multi-sheet support | Detailed analysis, report creation | + +--- + +## Related Documents + +- [Channel Management](./03-channel-management.md) - Channel and field settings for feedback collection +- [Issue Management](./05-issue-management.md) - Creating and managing issues from feedback +- [API Integration](/en/developer-guide/api-integration) - How to send feedback from external systems diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/05-issue-management.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/05-issue-management.md new file mode 100644 index 000000000..9d23431fb --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/05-issue-management.md @@ -0,0 +1,314 @@ +--- +title: Issue +description: This document explains how to create and manage issues in ABC User Feedback and efficiently track them using kanban/list views. +sidebar_position: 5 +--- + +# Issue + +**Issues** are a core feature for systematically managing problems or improvements found in feedback. This document covers all issue management features, from issue creation to category management and various view modes. + +![issue](/img/issue/1.png) + +--- + +## Issue Overview + +### Role of Issues + +Issues are used for the following purposes: + +- **Problem tracking**: Systematically manage bugs, errors, performance issues, etc. +- **Feature request management**: Structure user requests and reflect them in development plans +- **Identifying improvements**: Identify improvement points through feedback analysis + +### Issue Status + +Each issue has the following status: + +| Status | Description | When Used | +| --------------- | ------------------- | --------------------------------------- | +| **New** | Newly registered | Initial issue creation | +| **On Review** | Under review | When assignee starts review | +| **In Progress** | In progress | During actual work | +| **Resolved** | Resolved | Problem solved and completed | +| **On Hold** | Temporarily on hold | Waiting for additional info or deferred | + +--- + +## Creating/Editing/Deleting Issues + +### Issue Creation Methods + +Issues can be created in two ways. + +#### 1. Create Issue from Feedback (Recommended) + +The most common method, creating an issue based on specific feedback. + +1. Click feedback in the **Feedback** tab to open detail view +2. Click the **`+` button** in the **Issue** section of the right detail panel +3. Enter the issue name and press **Enter** or click the **Create** option + +#### 2. Create Directly from Issue List + +1. Click the **Issue** tab in the top menu +2. Click the **+ Create Issue** button at the top left +3. Enter information in the issue creation dialog: + +| Item | Description | Required | Example | +| --------------- | ----------------------------- | -------- | ---------------------------- | +| **Title** | Issue title | Required | `Login button malfunction` | +| **Description** | Detailed description | Optional | `Occurs in specific browser` | +| **Category** | Issue classification | Optional | `Bug` | +| **Status** | Initial status (default: New) | Optional | `New` | + +### Editing Issues + +You can modify information of created issues. + +#### Editing Method + +1. Click the issue you want to edit in the issue list +2. Click the **Edit** button in the **Issue Details** panel that opens on the right +3. In edit mode, you can modify the following items: + +#### Editable Items + +| Item | Editable | Description | +| --------------- | -------- | ------------------------------------------- | +| **Title** | ✅ Yes | Issue title | +| **Description** | ✅ Yes | Detailed description | +| **Category** | ✅ Yes | Issue classification (select from dropdown) | +| **Status** | ✅ Yes | Current progress status | +| **Ticket** | ✅ Yes | External issue tracker ticket number | +| **ID** | ❌ No | System auto-generated | +| **Created** | ❌ No | Creation date/time | + +#### Save and Cancel + +- **Save** button: Saves changes and exits edit mode +- **Cancel** button: Cancels changes and reverts to original state + +### External Issue Tracker Integration + +When integration with external issue trackers (Jira, etc.) is configured, you can link external tickets to issues. + +#### Ticket Linking Method + +1. Enter the external ticket number in the **Ticket** field in the issue detail panel +2. The entered number is automatically converted to an external system link + +> **Note**: External issue tracker integration requires prior configuration in **Settings > Issue Tracker Management**. + +### Deleting Issues + +You can delete issues that are no longer needed. + +#### Deletion Method + +1. Click the **Delete** button in the issue detail panel + +2. Approve deletion in the confirmation dialog + +#### Deletion Notes + +- **Cannot be recovered**: Deleted issues cannot be restored +- **Feedback links removed**: Issue links in connected feedback are removed +- **Statistics impact**: Data is excluded from dashboard issue statistics + +--- + +## Kanban View + +Kanban view is a viewing method that allows visual management by separating issues into columns by status. + +![issue-kanban](/img/issue/2.png) + +### Accessing Kanban View + +1. Click the **Issue** tab in the top menu +2. Select **Kanban** view at the top right + +### Kanban Board Structure + +Columns are organized by each status, and issues are displayed as cards. + +#### Kanban Column Structure + +| Column | Display Information | Card Count Display | +| --------------- | ----------------------- | ------------------ | +| **New** | Newly registered issues | Number at top | +| **On Review** | Issues under review | Number at top | +| **In Progress** | Issues in progress | Number at top | +| **Resolved** | Resolved issues | Number at top | +| **On Hold** | Issues on hold | Number at top | + +#### Issue Card Information + +Each issue card displays the following information: + +- **Issue title**: Click to go to detail view +- **Feedback count**: Number of linked feedback (with 📝 icon) +- **Category**: Displayed at bottom if set +- **External ticket**: Ticket number displayed if linked + +### Drag and Drop Status Change + +A core feature of kanban view, you can change status by dragging issue cards to different columns. + +#### Usage Method + +1. Click and drag an issue card with the mouse +2. Move it over the desired status column +3. Release the mouse to automatically change status + +### Kanban View Filtering + +You can display only issues matching specific conditions using the filter function at the top. + +#### Available Filters + +1. **Date** filter: Display only issues created in a specific period +2. **Filter** button: Set advanced filter conditions + +#### Filter Condition Examples + +| Filter Type | Condition Example | Use Case | +| ------------ | ---------------------- | ---------------------------------- | +| **Category** | Category = "Bug" | Check only bug issues | +| **Title** | Title contains "Login" | Find login-related issues | +| **Created** | Created >= 2024-03-01 | Issues created after specific date | +| **Status** | Status != "Resolved" | Display only unresolved issues | + +### Kanban View Sorting + +You can change the sort order of issue cards within each column. + +#### Sort Options + +- **Created Date ↓**: Newest first +- **Created Date ↑**: Oldest first +- **Feedback Count ↓**: Most linked feedback first + +--- + +## List View + +List view is a viewing method that displays issues grouped by category in table format. + +### Accessing List View + +1. Click the **Issue** tab in the top menu +2. Select **List** view at the top right + +### List View Structure + +Issues grouped by category are displayed hierarchically. + +#### Category Groups + +Each category is displayed as a collapsible/expandable group: + +- **Group header**: Category name and number of included issues +- **Collapse/Expand arrow**: Toggle group content display/hide +- **"No Category"**: Issues without assigned category + +### List View Filtering + +Provides the same filter function as kanban view. + +#### Filter Application Method + +1. Click the **Date** or **Filter** button at the top +2. Set desired conditions +3. Filtered results are displayed grouped by category + +#### Empty Category Handling + +Categories with no issues in the filtering results are automatically hidden. + +### List View Sorting + +You can sort by clicking each column header. + +#### Sort Behavior + +- **First click**: Ascending sort ↑ +- **Second click**: Descending sort ↓ + +#### Each Sort Content + +| Sort Criteria | Use Case | +| -------------------- | ----------------------------- | +| **Created ↓** | Check latest issues first | +| **Feedback Count ↓** | Prioritize high-impact issues | +| **Status** | Check grouped by status | + +--- + +## Issue Categories + +Issue categories are a feature that allows systematic management by classifying issues. + +### Purpose of Categories + +- **Issue classification**: Distinguish bugs, feature requests, improvements, etc. +- **Analysis ease**: Analyze issue occurrence patterns by category + +### Default Category Examples + +Commonly used category classifications: + +| Category | Description | Priority | Example Team | +| ------------------- | ----------------------------- | -------- | ---------------------------- | +| **Bug** | Function malfunction, errors | High | Development Team | +| **Feature Request** | New feature addition requests | Medium | Planning Team | +| **Improvement** | Existing feature enhancement | Medium | UX Team | +| **Performance** | Speed, stability issues | High | Infrastructure Team | +| **UI/UX** | User interface problems | Low | Design Team | +| **Documentation** | Help, guide related | Low | Technical Documentation Team | + +### Category Management + +#### Adding Categories + +You can add new categories in the issue detail panel: + +1. Click the **Add** button in the **Category** field of the issue detail panel +2. Enter the new category name +3. Press **Enter** or click the confirm button + +#### Assigning Categories + +You can assign or change categories for existing issues: + +1. Click the **Edit** button in the issue detail panel +2. Select the desired category from the **Category** dropdown +3. Save changes with the **Save** button + +### Category-Based Issue Management + +#### Checking by Category in List View + +In list view, you can see issues grouped by category at a glance: + +- **Issue count by category**: Number of included issues displayed in each group header +- **Group collapse/expand**: Selectively check only needed categories +- **"No Category" group**: Separate management of unclassified issues + +#### Category-Based Filtering + +When you want to check only issues of a specific category: + +1. Click the **Filter** button +2. Add a **Category** condition +3. Select the desired category + +--- + +## Related Documents + +- [Feedback Management](./04-feedback-management.md) - How to create and link issues from feedback +- [Issue Tracker Integration](/en/user-guide/settings/issue-tracker-management) - Integration settings with external tools +- [Project Management](./02-project-management.md) - Team composition and permission management diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/01-tenant-settings.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/01-tenant-settings.md new file mode 100644 index 000000000..68f2a7929 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/01-tenant-settings.md @@ -0,0 +1,236 @@ +--- +title: Tenant Settings +description: This guide explains how to manage tenant information, login methods, user management, and other organization-wide settings in ABC User Feedback. +sidebar_position: 1 +--- + +# Tenant Settings + +Tenant settings are the top-level management features of ABC User Feedback, covering important settings that affect the entire organization. This document explains how to manage tenant information, configure login methods, and manage all users. + +> **Note**: These settings can only be accessed by users with **Super Admin permissions**. + +--- + +## Tenant Settings + +A tenant is the top-level unit of an organization, encompassing all projects and users. + +### Access Method + +1. Click the **Home** icon in the top right menu +2. Select **Tenant Information** from the left menu + +### Editable Items + +| Item | Description | Editable | Example | +| --------------- | -------------------------------------------- | -------- | ---------------------------- | +| **ID** | Unique tenant identifier (system auto-generated) | ❌ No | `1` | +| **Name** | Tenant name (organization name, company name, etc.) | ✅ Yes | `ABC Company` | +| **Description** | Tenant description (optional) | ✅ Yes | `Customer feedback management system` | + +### How to Edit Information + +1. Modify the **Name** or **Description** field +2. Click the **Save** button at the top right +3. A success message is displayed when saved + +> The tenant name may be displayed in the login UI. + +--- + +## Login Settings + +Configure the authentication method users will use when accessing the system. + +### Access Method + +1. Click the **Home** icon in the top right menu +2. Select **Login Management** from the left menu + +### Supported Login Methods + +#### 1. Email Login + +The default email + password combination method. + +**Features**: + +- Enabled by default without additional setup +- User invitation → email verification → password setup sequence +- Password reset functionality provided + +**Password Policy**: + +- Minimum 8 characters +- Recommended to include letters, numbers, and special characters +- Consecutive characters prohibited (e.g., `aa`, `11`) + +#### 2. Google Login + +Social login method via Google OAuth 2.0. + +**Setup Method**: + +1. **Enable Google Login**: Toggle to ON +2. **Google Cloud Console setup is required**: + +> **Note**: For detailed implementation of Google OAuth integration, refer to the [OAuth Integration Guide](/en/developer-guide/oauth-integration). + +#### 3. Custom OAuth Login + +Method using your own OAuth server or other OAuth providers. + +**Setup Items**: + +| Item | Description | Example | +| ----------------- | ------------------------------------ | --------------------------------------------- | +| **Provider Name** | Name displayed on login button | `Sign in with Microsoft` | +| **Client ID** | OAuth client ID | `abc123xyz` | +| **Client Secret** | OAuth client secret | `supersecret` | +| **Auth URL** | Authentication request URL | `https://auth.example.com/oauth2/auth` | +| **Token URL** | Token request URL | `https://auth.example.com/oauth2/token` | +| **User Info URL** | User information request URL | `https://auth.example.com/oauth2/userinfo` | +| **Scope** | Permission scope to request | `openid email profile` | +| **Email Key** | Email field name in user information | `email` | + +**Setup Sequence**: + +1. Enter OAuth server information in each field +2. Click the **Save** button to save +3. A button with the configured Provider Name is displayed on the login screen + +### Login Method Combinations + +Multiple login methods can be enabled simultaneously: + +- **Email only**: Only default login form displayed +- **Email + Google**: Login form + "Sign in with Google" button +- **Email + Custom**: Login form + custom OAuth button + +### Testing Login Settings + +After changing settings, be sure to test: + +1. Access the login page in browser incognito mode +2. Verify that configured login methods are displayed correctly +3. Perform actual login tests with each method + +--- + +## User Management + +A feature to centrally manage all users across the tenant. + +### Access Method + +1. Click the **Home** icon in the top right menu +2. Select **User Management** from the left menu + +### Viewing User List + +#### Displayed Information + +| Column | Description | Display Example | +| ---------- | -------------------------------- | ---------------------------- | +| Email | Login account email | `user@company.com` | +| Name | User name (from profile) | `John Doe` | +| Department | Department | `Development Team` | +| Type | User type | `SUPER` / `GENERAL` | +| Project | List of accessible projects | `Project A, Project B` | +| Created | Account creation date/time | `2024-03-15 14:30` | + +#### User Type Description + +| Type | Description | Permission Scope | +| --------- | --------------------------------------------------------------- | ---------------------- | +| `SUPER` | Can access all projects and settings. Acts as full system administrator | Entire tenant | +| `GENERAL` | Can only access specified projects | Specific projects only | + +### User Search and Filtering + +You can quickly find desired users when there are many users. + +#### Filter Function + +Click the **Filter** button at the top to set conditions. + +**Filter Conditions**: + +- **Email**: Search by email address +- **Name**: Search by user name +- **Department**: Search by department name + +**Operator Options**: + +- **CONTAINS**: When it contains +- **IS**: When it exactly matches + +### Inviting Users + +Invite new users to the system. + +#### Invitation Method + +1. Click the **Invite User** button at the top right +2. Enter invitation information + +| Item | Description | Options | +| ----------- | ---------------------------------- | --------------------------- | +| **Email** | Email address of user to invite | Required input | +| **Type** | User type | `GENERAL` / `SUPER` | +| **Project** | Projects to allow access | Select from project list | +| **Role** | Role in that project | `Admin` / `Editor` / `Viewer` | + +3. Click the **Invite** button to complete the invitation + +#### Post-Invitation Process + +1. An email is sent to the invited user +2. The user clicks the link in the email to proceed with registration +3. After registration is complete, they are automatically added to the specified project + +### Editing User Information + +You can modify information and permissions of existing users. + +#### Editing Method + +1. Click the user you want to edit in the user list +2. The **Edit User** popup opens + +#### Editable Items + +| Item | Editable | Description | +| --------- | -------- | ------------------------------------- | +| **Email** | ❌ No | Cannot be changed as account identifier | +| **Type** | ✅ Yes | Can change between `GENERAL` ↔ `SUPER` | + +#### Save and Apply + +1. Modify necessary information +2. Click the **Save** button +3. Changes are applied immediately and reflected from the user's next login + +### Deleting Users + +You can delete users who no longer use the system. + +#### Deletion Method + +1. Click the **Delete** button at the bottom of the user edit popup +2. Approve deletion in the confirmation dialog + +#### Deletion Notes + +- **Cannot be recovered**: Deleted user accounts cannot be restored +- **Immediate access removal**: All system access is blocked immediately upon deletion + +--- + +## Related Documents + +- [Project Management](../02-project-management.md) - Member and permission management per project +- [OAuth Integration Guide](../../02-developer-guide/03-oauth-integration.md) - Technical implementation of OAuth settings +- [API Integration](/en/developer-guide/api-integration) - User management via API + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/02-api-key-management.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/02-api-key-management.md new file mode 100644 index 000000000..72b6c3795 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/02-api-key-management.md @@ -0,0 +1,134 @@ +--- +title: API Key Settings +description: This document explains how to create, manage, and maintain security for API keys used for external system integration in ABC User Feedback. +sidebar_position: 2 +--- + +# API Key Settings + +API keys are authentication means that allow external systems to securely integrate with ABC User Feedback. This document explains API key creation, management, and security maintenance from a UI perspective. + +![api-key-setting.png](/img/api-key/api-key-setting.png) + +--- + +## API Key Overview + +### Role of API Keys + +API keys are used for the following purposes: + +- **External system authentication**: Send feedback from websites, mobile apps +- **Automation integration**: Data collection through batch jobs, scripts +- **Third-party tool connection**: Integration with analysis tools, monitoring systems +- **Security control**: Independent access permission management per project + +### Security Features + +- **Per-project independence**: Separate keys issued for each project +- **Status management**: Can be controlled immediately with Active/Inactive status + +--- + +## Creating API Keys + +### Access Method + +1. Click **Settings** in the top menu +2. Select **API Key Management** from the left menu + +### Key Creation Process + +#### 1. Click Create Button + +Click the **Create API Key** button at the top right of the API Key Management screen. + +#### 2. Auto Generation and Display + +A new API key is automatically generated and displayed in a popup immediately upon clicking the button. + +**Popup Components**: + +- **API Key Value**: Displays the full key string +- **Copy Button**: Instantly copies to clipboard + +--- + +## Managing API Key List + +### Key List Screen Structure + +Created API keys are managed in table format. + +#### Table Column Information + +| Column | Description | Display Format | +| ----------- | ------------------------- | ----------------------------- | +| **API Key** | Generated key value | `AbcdEfgh...` | +| **Status** | Current activation status | Active / Inactive | +| **Created** | Key creation date/time | `2024-03-15 14:30` | +| **Actions** | Management action buttons | Status change, delete buttons | + +#### Key Identification Method + +Since the full key value cannot be viewed again, distinguish keys using the following methods: + +- **Creation time**: Check when the key was created +- **Usage purpose memo**: Record the key's purpose separately + +--- + +## API Key Status Management + +![api-key-detail.png](/img/api-key/api-key-detail.png) + +### Active / Inactive Toggle + +Each API key can be activated/deactivated immediately. + +#### Status Meanings + +| Status | Description | API Call Result | +| ------------ | ------------------------------ | ----------------- | +| **Active** | Available for actual API calls | Normal processing | +| **Inactive** | Call blocked state | 401 Unauthorized | + +#### Status Change Method + +1. Click the toggle switch in the **Status** column of the API key list +2. Status changes immediately and is reflected on screen +3. External systems using that key are immediately affected + +--- + +## Deleting API Keys + +### When to Delete + +API keys should be deleted in the following cases: + +- **Key exposure**: When a key is accidentally made public +- **Project termination**: When use of that project ends +- **Security policy**: According to regular key rotation policy +- **Unused keys**: Cleanup of keys no longer in use + +### Deletion Method + +#### 1. Click Delete Button + +Click the delete button in the **Actions** column of the key you want to delete in the key list. + +#### 2. Confirm Deletion + +Approve deletion in the confirmation dialog. + +#### 3. Deletion Complete + +When you click the **Delete** button, the key is immediately deleted and removed from the list. + +--- + +## Related Documents + +- [API Integration Guide](/en/developer-guide/api-integration) - Actual integration implementation using API keys +- [Project Management](/en/user-guide/project-management) - API key management per project diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/03-issue-tracker-management.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/03-issue-tracker-management.md new file mode 100644 index 000000000..cd01035f7 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/03-issue-tracker-management.md @@ -0,0 +1,167 @@ +--- +title: Issue Tracker Settings +description: This guide explains how to integrate ABC User Feedback issues with external issue trackers (Jira, etc.) to track and link issues. +sidebar_position: 3 +--- + +# Issue Tracker Settings + +With issue tracker settings, you can integrate ABC User Feedback issues with external issue management systems (Jira, etc.). You can link external ticket links to internal issues to naturally integrate with development workflows. + +--- + +## Issue Tracker Overview + +### Purpose of Integration + +Issue tracker integration is used for the following purposes: + +- **Workflow integration**: Connect customer feedback with development work +- **Issue tracking**: One-to-one mapping between internal issues and external tickets +- **Progress sharing**: Synchronize information between development and customer support teams +- **Efficiency improvement**: Prevent duplicate work and maintain context + +### Integration Method + +- **Manual link connection**: Manually enter external ticket numbers in ABC issues +- **Automatic URL generation**: Automatically generate links with configured Base URL and Project Key +- **Click navigation**: Click generated links to immediately navigate to external systems + +> **Note**: Real-time bidirectional synchronization is not supported. If synchronization such as status changes is needed, use [Webhook](./04-webhook-management.md). + +--- + +## Accessing Settings Screen + +### Access Method + +1. Click **Settings** in the top menu +2. Select **Issue Tracker Management** from the left menu + +### Settings Screen Structure + +The issue tracker management screen is structured as follows: + +- **Issue Tracking System**: Dropdown to select system to integrate +- **Connection Settings**: Area to enter connection information +- **Link Preview**: Preview of links to be generated +- **Test Connection**: Connection test button + +--- + +## Jira Integration Settings + +### System Selection + +#### 1. Issue Tracking System Setting + +Select **Jira** from the dropdown. + +### Entering Connection Information + +#### 2. Base URL Setting + +Enter the base address of the Jira system. + +| Input Example | Description | +| --------------------------------------- | ------------------------------ | +| `https://yourcompany.atlassian.net` | Jira Cloud instance | +| `https://jira.company.com` | Self-hosted Jira Server | +| `https://jira.internal:8080` | Internal network Jira | + +**Notes**: + +- Must include `https://` or `http://` protocol +- Remove trailing slash (`/`) +- Include port number if present + +#### 3. Project Key Setting + +Enter the unique key of the Jira project. + +| Input Example | Description | +| ------------- | --------------------------- | +| `PROJ` | Common project key | +| `DEV` | Development team project | +| `CS` | Customer support team project | +| `BUG` | Bug management dedicated | + +**How to Check Project Key**: + +1. Access the project in Jira +2. The part before `-` in the issue number is the Project Key +3. Example: In `PROJ-123`, `PROJ` is the Project Key + +### Link Preview + +You can preview the links that will be generated based on the configured information. + +#### Preview Structure + +``` +Base URL + /browse/ + Project Key + - + Issue Number +``` + +**Example**: + +- Base URL: `https://yourcompany.atlassian.net` +- Project Key: `PROJ` +- Issue Number: `123` (entered by user) +- **Generated Link**: `https://yourcompany.atlassian.net/browse/PROJ-123` + +#### Link Format Validation + +Use the preview to check the following: + +- Whether the URL format is correct +- Whether actual Jira issues are accessible +- Whether team members have access permissions + +--- + +## Linking Tickets to Issues + +### Entering Ticket Number + +After issue tracker settings are complete, you can link external tickets to individual issues. + +#### Linking Method + +1. Click the desired issue in the **Issue** tab +2. The **Issue Details** panel opens on the right +3. Enter the external ticket number in the **Ticket** field + +#### Input Format + +| Input Method | Description | Generated Link | +| ----------- | ----------- | -------------------------------------------- | +| `123` | Enter only number | `https://jira.company.com/browse/PROJ-123` | + +The system automatically adds the Project Key, so you only need to enter the number. + +### Automatic Link Generation + +After entering the ticket number, click the **Save** button to automatically generate the link. + +#### Link Features + +- **Click navigation**: Clicking the link opens the external Jira issue in a new tab +- **External link display**: External link icon displayed next to the link +- **Editable**: Can change ticket number in Edit mode + +### Unlinking + +To remove external ticket connection: + +1. Click the **Edit** button in the issue detail panel +2. Delete the content in the **Ticket** field +3. Save changes with the **Save** button + +--- + +## Related Documents + +- [Issue Management](/en/user-guide/issue-management) - How to create issues and link tickets +- [Webhook Management](/en/user-guide/settings/webhook-management) - External system notifications when issue status changes +- [API Integration Guide](/en/developer-guide/api-integration) - Issue management via API + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/04-webhook-management.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/04-webhook-management.md new file mode 100644 index 000000000..46579c75c --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/04-webhook-management.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 4 +title: 'Webhook Settings' +description: 'This document explains how to set up webhooks for automatic integration with external systems and send notifications when events occur.' +--- + +# Webhook Settings + +Webhooks are a feature that **automatically sends notifications to external systems when specific events occur** in ABC User Feedback. You can deliver events such as feedback creation and issue status changes in real-time to external services (Slack, Discord, your own server, etc.). For detailed integration guide, refer to the [Webhook Integration](/en/developer-guide/webhook-integration) document. + +![webhook-setting](/img/webhook/webhook-setting.png) + +--- + +## Access Method + +1. Click **Settings** in the top menu +2. Select **Webhook Integration** from the left menu + +--- + +## Webhook Integration Screen Overview + +![webhook-list.png](/img/webhook/webhook-list.png) + +The webhook integration screen is structured as follows: + +### Webhook List Table Structure + +| Column | Description | +| ----------------- | --------------------------------------------- | +| **On/Off** | Webhook activation/deactivation toggle switch | +| **Name** | Webhook name | +| **URL** | External endpoint to receive notifications | +| **Event Trigger** | Subscribed event triggers | +| **Created** | Webhook creation date/time | + +--- + +## Creating New Webhooks + +![webhook-create](/img/webhook/webhook-create.png) + +### 1. Start Webhook Registration + +Click the **Register Webhook** button at the top right to open the webhook registration modal. + +### 2. Enter Basic Information + +#### Required Input Items + +| Item | Description | +| -------- | -------------------------------------- | +| **Name** | Name for webhook identification | +| **URL** | Endpoint to receive HTTP POST requests | + +### 3. Token Setting (Optional) + +You can set a token for authentication in the **Token** field: + +- Click the **Generate** button to auto-generate +- Or enter token value directly + +### 4. Event Trigger Selection + +You can select events to subscribe to per channel: + +#### Supported Event Types + +For each channel (VOC, Review, Survey, VOC Test), you can select the following events: + +| Event Type | Description | +| ----------------------- | -------------------------------- | +| **Feedback Creation** | When new feedback is registered | +| **Issue Registration** | When new issue is created | +| **Issue Status Change** | When issue status is changed | +| **Issue Creation** | When issue is linked to feedback | + +### 5. Save Webhook + +After entering all information: + +1. Click the **OK** button to create the webhook +2. You can cancel with the **Cancel** button + +--- + +## Webhook Status Management + +### Activation/Deactivation Toggle + +You can change the status by clicking the toggle switch in the **On/Off** column of each webhook in the webhook list: + +- **On (Active)**: Real-time transmission when events occur +- **Off (Inactive)**: Webhook is maintained but transmission is stopped + +### Temporary Deactivation Scenarios + +- When external server is under maintenance +- When changing webhook URL +- When spam notifications need to be prevented + +--- + +## Webhook Editing and Deletion + +### Webhook Editing + +Click the webhook you want to edit in the webhook list to open the edit modal: + +#### Editable Items + +- Webhook name +- Target URL change +- Token value modification +- Event type addition/removal + +### Webhook Deletion + +To completely remove a webhook: + +1. Select delete option in the edit modal +2. Or click delete button directly in the list (if delete button exists in UI) + +--- + +## Webhook Testing and Validation + +### Manual Validation Method + +1. **Feedback Creation Test**: + - Register test feedback to check `Feedback Creation` event +2. **Issue Management Test**: + - Create issues or change status to check related events +3. **External Service Check**: + - Check message reception in Slack, Discord, etc. + +--- + +## Common Integration Examples + +### Slack Webhook Settings + +``` +Name: Slack Notifications +URL: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX +Events: Feedback Creation, Issue Registration (all channels) +``` + +### Discord Webhook Settings + +``` +Name: Discord Development Team Notifications +URL: https://discord.com/api/webhooks/123456789/abcdefghijk +Events: Issue Status Change (VOC channel only) +``` + +### Custom Server Integration + +``` +Name: Internal Analysis System +URL: https://api.yourcompany.com/webhooks/feedback +Token: your-generated-token +Events: All events (all channels) +``` + +--- + +## Related Documents + +- [Webhook Developer Guide](/en/developer-guide/webhook-integration) - How to implement webhook receiving server +- [API Key Management](./02-api-key-management.md) - API key-based authentication settings diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/05-ai-setting.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/05-ai-setting.md new file mode 100644 index 000000000..37857d83c --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/05-ai-setting.md @@ -0,0 +1,272 @@ +--- +sidebar_position: 5 +title: 'AI Settings' +description: 'This document explains basic settings and integration methods for using generative AI features.' +--- + +# AI Settings + +To use **generative AI features** in ABC User Feedback, you must first set up integration with an AI provider. +After completing AI settings, you can use all features such as **AI Field Templates**, **AI Issue Recommendations**, and **AI Usage Monitoring**. + +--- + +## Access Method + +1. Click **Settings** in the top menu +2. Select **Generative AI Integration** from the left menu +3. Click **AI Setting** in the top tabs + +--- + +## AI Provider Selection and Settings + +![ai-setting.png](/img/ai/ai-setting.png) + +### 1. Provider Selection + +Select one of the currently supported AI providers: OpenAI, Google Gemini: + +### 2. API Key Input + +Enter the API key issued from the selected AI provider. + +### 3. Base URL Setting (Optional) + +In most cases, **leave it empty** and the default value will be used automatically. + +Enter only when using special endpoints or proxy servers. + +### 4. System Prompt Setting (Optional) + +You can set **basic instructions** that the AI will reference when processing all requests. + +Use this when you have organizational tone and manner or special requirements. + +### 5. Save Settings + +After entering all information, click the **Save** button at the top right. + +--- + +## AI Usage Monitoring + +![ai-usage.png](/img/ai/ai-usage.png) + +You can monitor AI feature usage and costs in the **AI Usage** tab. + +### Usage Dashboard + +Information you can check: + +- **Daily/monthly API call count** +- **Token usage** (input/output separately) +- **Usage distribution by feature** (AI fields vs issue recommendations) + +--- + +## AI Field Template Management + +![ai-field-template.png](/img/ai/ai-field-template.png) + +After completing AI settings, you can manage automatic feedback analysis templates in the **AI Field Template** tab. + +### Default Templates + +Default templates provided by the system: + +| Template | Description | Usage Example | +| ---------------------- | ---------------------------------------------- | ---------------------------------------- | +| **Feedback Summary** | Summarize feedback in one sentence | Grasp core of long feedback | +| **Sentiment Analysis** | Sentiment analysis (positive/negative/neutral) | Analyze customer satisfaction trends | +| **Translation** | Translate feedback to English | Integrate multilingual feedback analysis | +| **Keyword Extraction** | Extract 2-3 core keywords | Auto-tag issue categories | + +### Creating Custom Templates + +![ai-field-template-create.png](/img/ai/ai-field-template-create.png) + +1. Click the **Create New** card +2. Enter template information + +| Item | Description | +| --------------- | ------------------------------- | +| **Title** | Template name | +| **Prompt** | Instruction sentence for AI | +| **Model** | Select AI model to use | +| **Temperature** | Creativity adjustment (0.0~1.0) | + +3. Test in Playground + +- Enter test feedback with "Add Data" button +- Check results by clicking "AI test execution" + +### Template Editing and Deletion + +- Click template card → edit +- Delete with **Delete Template** button +- Deletion may affect AI fields using that template + +--- + +## Applying AI Fields to Channels + +After creating AI field templates, you must apply them as actual channel fields to view AI analysis results in feedback. + +### 1. Add AI Field in Field Management + +Add AI fields in **Settings > Channel List > [Select Channel] > Field Management**. + +#### AI Field Setting Items + +| Item | Description | Required | +| ----------------------- | -------------------------------- | -------- | +| **Key** | Unique field identifier | Required | +| **Display Name** | Name displayed in UI | Required | +| **Format** | Select `aiField` | Required | +| **Template** | Select created AI field template | Required | +| **Target Field** | Text field to be analyzed | Required | +| **Property** | Editable or Read Only | Required | +| **AI Field Automation** | Auto execution | Optional | + +#### Setting Example + +``` +Key: sentiment_analysis +Display Name: Sentiment Analysis +Format: aiField +Template: Feedback Sentiment Analysis +Target Field: message +Property: Read Only +AI Field Automation: ON (auto execution) +``` + +### 2. Template Connection and Target Field Setting + +Select the AI field template created earlier from the **Template** dropdown. + +**Target Field** specifies the fields to be analyzed by AI + +### 3. AI Field Automation Setting + +Select execution method through **AI Field Automation** toggle: + +- **ON (Auto execution)**: Automatically execute AI analysis when new feedback is registered +- **OFF (Manual execution)**: User must manually click execution button + +## Checking AI Analysis Results in Feedback + +After AI field settings are complete, you can check AI analysis results in the feedback list and detail screens. + +### Checking in Feedback List + +AI fields are added as new columns in the feedback table: + +- **Summary**: Summary generated by AI +- **Classification**: AI classification results +- **Korean**: Translation results, etc. + +### Checking in Feedback Detail Screen + +You can check more detailed AI analysis results in the feedback detail view panel: + +1. Click feedback row → right detail panel opens +2. Check AI analysis results by field +3. Displayed with analysis results for each AI field + +## Manual AI Analysis Execution + +You can manually execute AI analysis in the feedback detail screen. + +### Using Run AI Button + +1. Click **Run AI** button in feedback detail screen +2. AI analysis executes and results are automatically entered in that field +3. Results can be checked immediately after analysis completes + +### Manual Execution Usage Scenarios + +- **Cost savings**: Select only needed feedback for AI analysis +- **Performance check**: Test results of new templates in advance +- **Re-analysis**: Re-analyze existing feedback after template modification + +--- + +## AI Issue Recommendation Settings + +![ai-issue-recommendation.png](/img/ai/ai-issue-recommendation.png) + +You can set up automatic issue recommendation features based on feedback in the **AI Issue Recommendation** tab. + +### Creating AI Issue Recommendation Settings + +![ai-issue-recommendation-create.png](/img/ai/ai-issue-recommendation-create.png) + +1. Click **Create New** button +2. Enter setting items + +| Item | Description | Required | +| ---------------- | -------------------------------- | -------- | +| **Channel** | Select channel to apply | Required | +| **Target Field** | Field to analyze (e.g., message) | Required | +| **Prompt** | Recommendation criteria prompt | Optional | +| **Enable** | Feature activation toggle | Required | + +3. Advanced Settings + +| Setting | Description | +| ------------------------- | ----------------------------------------------------------- | +| **Model** | Model to use | +| **Temperature** | Creativity adjustment | +| **Data Reference Amount** | Amount of issues to reference (issues and related feedback) | + +### Testing AI Issue Recommendation Feature + +Test in Playground with entered settings: + +1. Enter example feedback +2. Click "AI test execution" +3. Check recommended issue list + +### Using Recommendations in Actual Feedback + +In feedback detail view: + +- Check AI recommended issue list +- Select appropriate issues with checkboxes +- Request different recommendations with **Retry** button + +### Using Issue Recommendations in Feedback List + +In channels with AI issue recommendations configured, you can also use issue recommendation features directly in the feedback list screen. + +#### Usage Method + +1. Click the **+ button** in the **Issue column** of the feedback you want to link issues to in the feedback list +2. When dropdown menu appears, select **"Run AI"** +3. AI analyzes related issues and displays recommendation list + +#### Checking and Applying Recommendation Results + +After AI analysis completes, from the recommended issue list: + +- Check **recommended issues** +- Select appropriate recommended issues +- Option to create new issues also provided +- After selection, that issue is automatically linked to feedback + +#### Batch Processing Usage + +You can use AI issue recommendations even when multiple feedback are selected, enabling efficient feedback classification: + +1. Select multiple rows in feedback list (using checkboxes) +2. Execute AI issue recommendations from top batch operation menu +3. Check and apply recommended issues for each feedback + +--- + +## Related Documents + +- [Field Settings](/en/user-guide/feedback-management) - How to apply AI fields to channels +- [Issue Creation and Status Management](/en/user-guide/issue-management) - How to use AI recommended issues +- [Feedback Checking and Filtering](/en/user-guide/feedback-management) - How to check AI analysis results diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/06-image-setting.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/06-image-setting.md new file mode 100644 index 000000000..928fb1359 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/06-image-setting.md @@ -0,0 +1,102 @@ +--- +sidebar_position: 6 +title: 'Image Settings' +description: 'This guide explains how to set up image storage methods and security policies for images attached to feedback.' +--- + +# Image Settings + +ABC User Feedback supports **uploading images along with feedback** when users submit feedback. You can build a safe and efficient feedback collection environment by appropriately setting image storage methods and security policies. + +![image-setting.png](/img/image/image-setting.png) + +--- + +## Access Method + +1. Click **Settings** in the top menu +2. Select **Channel List > [Select Channel]** from the left menu +3. Select **Image Management** from the bottom tabs + +--- + +## Image Storage Integration Settings + +S3 or S3-compatible storage integration is required to upload images directly to the server via **Multipart Upload API** or use **Presigned URL Download** functionality. + +### Required Setting Items + +| Item | Description | Example | +| --------------------- | ------------------------------------- | ----------------------------------------- | +| **Access Key ID** | Key ID for S3 access | `AKIAIOSFODNN7EXAMPLE` | +| **Secret Access Key** | Secret for the key | `wJalrXUtnFEMI/K7MDENG/...` | +| **End Point** | S3 API endpoint URL | `https://s3.ap-northeast-1.amazonaws.com` | +| **Region** | Region where bucket is located | `ap-northeast-1` | +| **Bucket Name** | Target bucket where images are stored | `consumer-ufb-images` | + +### Presigned URL Download Settings + +You can enhance image download security through the **Presigned URL Download** option. + +#### Setting Options + +- **Enable**: Access images through authenticated one-time URLs (enhanced security) +- **Disable**: Image URLs are directly exposed and publicly accessible + +### Connection Test + +After entering all settings, click the **Test Connection** button to verify storage connection. + +Connection results: + +- ✅ **Success**: "Connection test succeeded" message +- ❌ **Failure**: Recheck input values, bucket permissions, network settings + +--- + +## Image URL Domain Whitelist Settings + +When using **Image URL method** or wanting to enhance security, you can set a whitelist to allow only trusted domains. + +### Current Status Check + +Default setting is **"All image URLs are allowed"** state, allowing image URLs from all domains. + +### Adding to Whitelist + +To allow only specific domains for security enhancement: + +1. Add trusted domains in the **Whitelist** area +2. Example domains: + - `cdn.yourcompany.com` + - `images.trusted-partner.io` + - `storage.googleapis.com` + +--- + +## Supported Storage Services + +### AWS S3 + +- Most commonly used cloud storage +- Stable and highly scalable + +--- + +## Saving Settings + +After completing all settings, click the **Save** button at the top right to save changes. + +After saving: + +- New image uploads work according to configured method +- Existing images remain with existing settings +- Recommended to recheck normal operation with Test Connection + +--- + +## Related Documents + +- [Field Settings](/en/user-guide/feedback-management) - How to add image fields to feedback forms +- [Feedback Checking and Filtering](/en/user-guide/feedback-management) - How to check uploaded images in feedback +- [API Key Management](./02-api-key-management.md) - API key security management methods diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/_category_.json b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/_category_.json new file mode 100644 index 000000000..d4b99b40e --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 7, + "label": "Settings", + "description": "Guide for settings." +} diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/index.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/index.md new file mode 100644 index 000000000..9042ea370 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/index.md @@ -0,0 +1,8 @@ +--- +title: Settings +description: Guide for settings. +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/_category_.json b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/_category_.json new file mode 100644 index 000000000..2f7dae4fb --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 2, + "label": "User Guide" +} + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/index.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/index.md new file mode 100644 index 000000000..a7c0ac16e --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/01-user-guide/index.md @@ -0,0 +1,7 @@ +--- +title: User Guide +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/01-docker-hub-images.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/01-docker-hub-images.md new file mode 100644 index 000000000..c882cfaec --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/01-docker-hub-images.md @@ -0,0 +1,375 @@ +--- +id: docker-hub-images +title: Docker Hub Image Installation +description: This document explains how to quickly install the system using the official ABC User Feedback images registered on Docker Hub. +sidebar_position: 1 +--- + +# Docker Hub Image Installation + +ABC User Feedback provides official Docker images. +This document explains how to quickly set up the system locally using Docker Compose, including **Web UI, API server, database, SMTP server**, etc. + +--- + +## 1. Prerequisites + +| Item | Description | +| -------------- | ------------------------------------------------------------------------ | +| Docker | 20.10 or higher | +| Docker Compose | v2 or higher recommended | +| Ports Used | `3000`, `4000`, `13306`, `5080`, `25` (must be free on local machine) | + +--- + +## 2. Docker Image Configuration + +| Service Name | Description | Docker Image Name | +| ----------------- | ----------------------------------------- | ------------------------------------ | +| Web (Admin UI) | Frontend web UI (Next.js) | `line/abc-user-feedback-web` | +| API (Backend) | Backend server (NestJS) | `line/abc-user-feedback-api` | +| MySQL | Database | `mysql:8.0` | +| SMTP4Dev | Email server for local testing | `rnwood/smtp4dev:v3` | +| (Optional) OpenSearch | For search functionality and improved AI analysis accuracy | `opensearchproject/opensearch:2.16.0` | + +--- + +## 3. `docker-compose.yml` Example + +```yaml +name: abc-user-feedback +services: + web: + image: line/abc-user-feedback-web:latest + environment: + - NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 + ports: + - 3000:3000 + depends_on: + - api + restart: unless-stopped + + api: + image: line/abc-user-feedback-api:latest + environment: + - JWT_SECRET=jwtsecretjwtsecretjwtsecret + - MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@mysql:3306/userfeedback + - SMTP_HOST=smtp4dev + - SMTP_PORT=25 + - SMTP_SENDER=user@feedback.com + # Uncomment below if using OpenSearch + # - OPENSEARCH_USE=true + # - OPENSEARCH_NODE=http://opensearch-node:9200 + ports: + - 4000:4000 + depends_on: + - mysql + restart: unless-stopped + + mysql: + image: mysql:8.0 + command: + [ + "--default-authentication-plugin=mysql_native_password", + "--collation-server=utf8mb4_bin", + ] + environment: + MYSQL_ROOT_PASSWORD: userfeedback + MYSQL_DATABASE: userfeedback + MYSQL_USER: userfeedback + MYSQL_PASSWORD: userfeedback + TZ: UTC + ports: + - 13306:3306 + volumes: + - mysql:/var/lib/mysql + restart: unless-stopped + + smtp4dev: + image: rnwood/smtp4dev:v3 + ports: + - 5080:80 + - 25:25 + - 143:143 + volumes: + - smtp4dev:/smtp4dev + restart: unless-stopped + + # Uncomment below if you want to use OpenSearch + # opensearch-node: + # image: opensearchproject/opensearch:2.16.0 + # restart: unless-stopped + # environment: + # - cluster.name=opensearch-cluster + # - node.name=opensearch-node + # - discovery.type=single-node + # - bootstrap.memory_lock=true + # - 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m' + # - plugins.security.disabled=true + # - OPENSEARCH_INITIAL_ADMIN_PASSWORD=UserFeedback123!@# + # ulimits: + # memlock: + # soft: -1 + # hard: -1 + # nofile: + # soft: 65536 + # hard: 65536 + # volumes: + # - opensearch:/usr/share/opensearch/data + # ports: + # - 9200:9200 + # - 9600:9600 + +volumes: + mysql: + smtp4dev: + # opensearch: +``` + +--- + +## 4. Execution Steps + +### 4.1 Download and Run Docker Images + +```bash +# Run all services in the background using Docker Compose +docker compose up -d +``` + +### 4.2 Check Running Status + +```bash +# Check if all containers are running normally +docker compose ps +``` + +### 4.3 Check Service Access + +- **Web Application**: [http://localhost:3000](http://localhost:3000) +- **API Server**: [http://localhost:4000](http://localhost:4000) +- **SMTP Test Page**: [http://localhost:5080](http://localhost:5080) +- **MySQL Database**: `localhost:13306` (user: `userfeedback`, password: `userfeedback`) + +--- + +## 5. SMTP Configuration + +By default, this configuration allows you to test emails through `smtp4dev`. + +- **Web Interface**: [http://localhost:5080](http://localhost:5080) +- **SMTP Port**: `25` +- **IMAP Port**: `143` + +### SMTP Testing Method + +1. Register a user or use the user invitation feature in the web application +2. Check sent emails at [http://localhost:5080](http://localhost:5080) +3. Test email content and attachments + +> **Important**: In actual production environments, you must integrate with an external SMTP server (e.g., Gmail, SendGrid, corporate SMTP, etc.). + +## 6. Installation Verification + +### 6.1 Web Application Access Verification + +Access `http://localhost:3000` in your browser and verify: + +- Tenant creation screen displays normally +- Page loading completes +- No JavaScript errors (check in browser developer tools) + +### 6.2 API Server Status Check + +```bash +# API server health check +curl http://localhost:4000/api/health +``` + +Expected response: + +```json +{ + "status": "ok", + "info": { + "database": { + "status": "up" + } + } +} +``` + +### 6.3 Database Connection Verification + +```bash +# Directly access MySQL container to check database +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "SHOW DATABASES;" + +# Check table creation +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "USE userfeedback; SHOW TABLES;" +``` + +### 6.4 Log Check + +```bash +# Check logs for all services +docker compose logs + +# Check logs for specific service only +docker compose logs api +docker compose logs web +docker compose logs mysql +``` + +--- + +## 7. OpenSearch Usage Notes + +OpenSearch is an optional component that improves search functionality and AI analysis accuracy. + +### 7.1 How to Enable OpenSearch + +1. Uncomment environment variables in the `api` service in `docker-compose.yml`: + +```yaml +- OPENSEARCH_USE=true +- OPENSEARCH_NODE=http://opensearch-node:9200 +``` + +2. Uncomment the `opensearch-node` service +3. Uncomment `opensearch:` in the `volumes:` section +4. Ensure ports `9200`, `9600` are not in use on local machine + +### 7.2 Memory Requirements + +> **Warning**: OpenSearch requires at least 2GB of memory. If memory is insufficient, the container may automatically terminate. + +### 7.3 OpenSearch Status Check + +```bash +# Check OpenSearch cluster status +curl http://localhost:9200/_cluster/health + +# Check OpenSearch node information +curl http://localhost:9200/_nodes + +# Check indices +curl http://localhost:9200/_cat/indices +``` + +### 7.4 Disabling OpenSearch + +To not use OpenSearch, comment out the corresponding service and environment variables in `docker-compose.yml`. + +--- + +## 8. Troubleshooting + +### 8.1 Port Conflict Issue + +**Symptom**: Port binding error occurs when running `docker compose up` + +**Solution**: + +```bash +# Check ports in use +lsof -i :3000 # Web port +lsof -i :4000 # API port +lsof -i :13306 # MySQL port +lsof -i :5080 # SMTP port + +# Stop processes using those ports and restart +docker compose down +docker compose up -d +``` + +### 8.2 Container Startup Failure + +**Symptom**: Some containers fail to start or keep restarting + +**Solution**: + +```bash +# Check container status +docker compose ps + +# Check logs for failed container +docker compose logs [service-name] + +# Stop and remove all containers +docker compose down + +# Remove volumes as well (warning: data loss) +docker compose down -v + +# Start again +docker compose up -d +``` + +### 8.3 Database Connection Error + +**Symptom**: MySQL connection failure from API server + +**Solution**: + +```bash +# Wait until MySQL container is fully started +docker compose logs mysql + +# Test direct connection to MySQL container +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "SELECT 1;" + +# Restart API service +docker compose restart api +``` + +### 8.4 Image Download Failure + +**Symptom**: Cannot download Docker images + +**Solution**: + +```bash +# Check Docker Hub login +docker login + +# Manually download images +docker pull line/abc-user-feedback-web:latest +docker pull line/abc-user-feedback-api:latest + +# Check network connection +ping hub.docker.com +``` + +### 8.5 Memory Insufficient Issue + +**Symptom**: OpenSearch container automatically terminates + +**Solution**: + +```bash +# Check system memory +free -h + +# Check Docker memory usage +docker stats + +# Disable OpenSearch (comment out in docker-compose.yml) +# Or increase memory allocation +``` + +--- + +## 9. Reference Links + +- [ABC User Feedback Web - Docker Hub](https://hub.docker.com/r/line/abc-user-feedback-web) +- [ABC User Feedback API - Docker Hub](https://hub.docker.com/r/line/abc-user-feedback-api) +- [smtp4dev - Docker Hub](https://hub.docker.com/r/rnwood/smtp4dev) +- [OpenSearch - Docker Hub](https://hub.docker.com/r/opensearchproject/opensearch) + +--- + +## Related Documents + +- [Initial Setup Guide](/en/user-guide/getting-started) + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/02-cli-tool.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/02-cli-tool.md new file mode 100644 index 000000000..93cfd6983 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/02-cli-tool.md @@ -0,0 +1,273 @@ +--- +sidebar_position: 2 +title: "CLI Tool Usage" +description: "This document explains how to quickly and easily install and manage the system using the ABC User Feedback CLI tool." +--- + +# CLI Tool Usage + +The ABC User Feedback CLI (`auf-cli`) is a command-line tool that simplifies system installation, execution, and management. As long as Node.js and Docker are installed, you can run it immediately via `npx` without installing additional dependencies or cloning the repository. + +## Main Features + +- Automatic setup of required infrastructure (MySQL, SMTP, OpenSearch) +- Simplified environment variable configuration +- Automatic start/stop of API and web servers +- Volume data cleanup +- Dynamic Docker Compose file generation + +## Docker Images Used + +- `line/abc-user-feedback-web:latest` - Web frontend +- `line/abc-user-feedback-api:latest` - API backend +- `mysql:8.0` - Database +- `rnwood/smtp4dev:v3` - SMTP test server +- `opensearchproject/opensearch:2.16.0` - Search engine (optional) + +## Prerequisites + +Before using the CLI tool, you must meet the following requirements: + +- [Node.js v22 or higher](https://nodejs.org/en/download/) +- [Docker](https://docs.docker.com/desktop/) + +## Basic Commands + +### Initialization + +To set up the infrastructure required for ABC User Feedback, run the following command: + +```bash +npx auf-cli init +``` + +This command performs the following tasks: + +1. Creates `config.toml` file for environment variable configuration +2. Sets up required infrastructure according to architecture (ARM/AMD) + +After initialization is complete, a `config.toml` file is created in the current directory. You can edit this file as needed to adjust environment variables. + +### Starting Server + +To start the API and web servers, run the following command: + +```bash +npx auf-cli start +``` + +This command performs the following tasks: + +1. Reads environment variables from `config.toml` file +2. Generates Docker Compose file and starts services +3. Starts API and web server containers and required infrastructure (MySQL, SMTP, OpenSearch) + +After the server starts successfully, you can access the ABC User Feedback web interface at `http://localhost:3000` (or configured URL) in your web browser. The CLI displays the following URLs: + +- Web interface URL +- API URL +- MySQL connection string +- OpenSearch URL (if enabled) +- SMTP web interface (when using smtp4dev) + +### Stopping Server + +To stop the API and web servers, run the following command: + +```bash +npx auf-cli stop +``` + +This command stops running API and web server containers and infrastructure containers. All data stored in volumes is preserved. + +### Volume Cleanup + +To clean up Docker volumes created during startup, run the following command: + +```bash +npx auf-cli clean +``` + +This command stops all containers and deletes Docker volumes for MySQL, SMTP, OpenSearch, etc. + +**Warning**: This operation deletes all data, so back up if needed. + +You can also clean up unused Docker images using the `--images` option: + +```bash +npx auf-cli clean --images +``` + +## Configuration File (config.toml) + +Running the `init` command creates a `config.toml` file in the current directory. This file is used to configure environment variables for ABC User Feedback. + +The following is an example of a `config.toml` file: + +```toml +[web] +port = 3000 +# api_base_url = "http://localhost:4000" + +[api] +port = 4000 +jwt_secret = "jwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecret" + +# master_api_key = "MASTER_KEY" +# access_token_expired_time = "10m" +# refresh_token_expired_time = "1h" + +# [api.auto_feedback_deletion] +# enabled = true +# period_days = 365 + +# [api.smtp] +# host = "smtp4dev" # SMTP_HOST +# port = 25 # SMTP_PORT +# sender = "user@feedback.com" +# username= +# password= +# tls= +# cipher_spec= +# opportunitic_tls= + +# [api.opensearch] +# enabled = true + +[mysql] +port = 13306 +``` + +You can edit this file as needed to adjust environment variables. For detailed information on environment variables, refer to the [Environment Variable Settings](./05-configuration.md) document. + +## Advanced Usage + +### Changing Ports + +By default, the web server uses port 3000 and the API server uses port 4000. To change these, modify the following settings in the `config.toml` file: + +```toml +[web] +port = 8000 # Change web server port +api_base_url = "http://localhost:8080" # API URL must also be changed + +[api] +port = 8080 # Change API server port + +[mysql] +port = 13307 # Change MySQL port if needed +``` + +### Enabling OpenSearch + +To enable OpenSearch for advanced search features: + +```toml +[api.opensearch] +enabled = true +``` + +**Notes**: + +- OpenSearch requires at least 2GB of available memory +- OpenSearch container is available at `http://localhost:9200` +- Check OpenSearch status: `http://localhost:9200/_cluster/health` + +### SMTP Settings + +For development environments, the default `smtp4dev` settings are recommended: + +```toml +[api.smtp] +host = "smtp4dev" +port = 25 +sender = "dev@feedback.local" +``` + +The smtp4dev web interface is available at `http://localhost:5080` to check sent emails. + +## Troubleshooting + +### Common Issues + +1. **Docker-related errors**: + + - Check if Docker is running: `docker --version` + - Check Docker permissions: `docker ps` + - Verify Docker Desktop is properly installed and running + +2. **Port conflicts**: + + - Check port usage: `lsof -i :PORT` (macOS/Linux) or `netstat -ano | findstr :PORT` (Windows) + - Change port settings in `config.toml` + - Common conflicting ports: 3000, 4000, 13306, 9200, 5080 + +3. **Service startup failure**: + + - Check container logs: `docker compose logs SERVICE_NAME` + - Verify Docker images are available: `docker images` + - Check sufficient system resources (memory, disk space) + +4. **Database connection issues**: + - Check MySQL container status: `docker compose ps mysql` + - Check MySQL logs: `docker compose logs mysql` + - Test connection: `docker compose exec mysql mysql -u userfeedback -p` + +### Debugging Tips + +1. **Check container logs**: + + ```bash + # All container logs + docker compose logs + + # Specific service logs + docker compose logs api + docker compose logs web + docker compose logs mysql + ``` + +2. **Check service status**: + + ```bash + # Check API status + curl http://localhost:4000/api/health + + # Check OpenSearch status (if enabled) + curl http://localhost:9200/_cluster/health + ``` + +3. **Direct database access**: + ```bash + # Connect to MySQL + docker compose exec mysql mysql -u userfeedback -p userfeedback + ``` + +## Limitations + +The CLI tool is designed for development and testing environments. For production deployment, consider the following: + +1. **Security Considerations**: + + - Use environment variables instead of configuration files for sensitive data + - Implement proper secret management + - Use production-grade JWT secrets + - Enable HTTPS/TLS encryption + +2. **Scalability and Availability**: + + - Use orchestration tools like Kubernetes or Docker Swarm + - Implement load balancing and auto-scaling + - Set up proper monitoring and alerts + - Use managed database services (RDS, Cloud SQL, etc.) + +3. **Data Management**: + - Implement automated backup strategies + - Use persistent volumes with proper backups + - Consider data retention policies + - Monitor disk usage and performance + +## Next Steps + +For detailed API and web server configuration options, refer to the [Environment Variable Settings](./05-configuration.md) document. + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/03-manual-setup.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/03-manual-setup.md new file mode 100644 index 000000000..5a68baeb1 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/03-manual-setup.md @@ -0,0 +1,276 @@ +--- +sidebar_position: 3 +title: 'Manual Installation' +description: 'Manual installation guide for building and running ABC User Feedback directly from source code' +--- + +# Manual Installation + +This document explains how to manually install and configure ABC User Feedback. This is useful when you want to build and run the application directly from source code. + +## Prerequisites + +Before proceeding with manual installation, you must meet the following requirements: + +- [Node.js v22.19.0 or higher](https://nodejs.org/en/download/) +- [pnpm v10.15.0 or higher](https://pnpm.io/installation) (package manager) +- [Git](https://git-scm.com/downloads) +- [MySQL 8.0](https://www.mysql.com/downloads/) +- SMTP server +- (Optional) [OpenSearch 2.16](https://opensearch.org/) + +## Downloading Source Code + +First, clone the ABC User Feedback source code from the GitHub repository: + +```bash +git clone https://github.com/line/abc-user-feedback.git +cd abc-user-feedback +``` + +## Infrastructure Setup + +ABC User Feedback requires a MySQL database, SMTP server, and optionally OpenSearch. There are several ways to set up these infrastructure components. + +### Infrastructure Setup Using Docker + +The simplest method is to set up required infrastructure with Docker Compose: + +```bash +docker-compose -f docker/docker-compose.infra.yml up -d +``` + +### Using Existing Infrastructure + +If you already have MySQL, OpenSearch, or SMTP server, you can configure connection information as environment variables later. + +## Installing Dependencies + +ABC User Feedback uses a monorepo structure managed through TurboRepo. To install dependencies for all packages: + +```bash +pnpm install +``` + +After installing dependencies, build all packages: + +```bash +pnpm build +``` + +## Environment Variable Configuration + +### API Server Environment Variables + +Create a `.env` file in the `apps/api` directory and configure it by referring to `.env.example`: + +```env +# Required environment variables +JWT_SECRET=DEV + +MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@localhost:13306/userfeedback # required + +ACCESS_TOKEN_EXPIRED_TIME=10m # default: 10m +REFRESH_TOKEN_EXPIRED_TIME=1h # default: 1h + +# Optional environment variables + +# APP_PORT=4000 # default: 4000 +# APP_ADDRESS=0.0.0.0 # default: 0.0.0.0 + +# MYSQL_SECONDARY_URLS= ["mysql://userfeedback:userfeedback@localhost:13306/userfeedback"] # optional + +SMTP_HOST=localhost # required +SMTP_PORT=25 # required +SMTP_SENDER=user@feedback.com # required +# SMTP_USERNAME= # optional +# SMTP_PASSWORD= # optional +# SMTP_TLS= # default: false +# SMTP_CIPHER_SPEC= # default: TLSv1.2 if SMTP_TLS=true +# SMTP_OPPORTUNISTIC_TLS= # default: true if SMTP_TLS=true + +# OPENSEARCH_USE=false # default: false +# OPENSEARCH_NODE= # required if OPENSEARCH_USE=true +# OPENSEARCH_USERNAME= # optional +# OPENSEARCH_PASSWORD= # optional + +# AUTO_MIGRATION=true # default: true + +# MASTER_API_KEY= # default: none + +# BASE_URL=https://api.example.com # Public API server URL used in Swagger documentation (optional) + +# AUTO_FEEDBACK_DELETION_ENABLED=false # default: false +# AUTO_FEEDBACK_DELETION_PERIOD_DAYS=365*5 +``` + +### Web Server Environment Variables + +Create a `.env` file in the `apps/web` directory and configure it by referring to `.env.example`: + +```env +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +``` + +For detailed information on environment variables, refer to the [Environment Variable Settings](./05-configuration.md) document. + +## Database Migration + +Before running the API server for the first time, you need to create the database schema. If you set the `AUTO_MIGRATION=true` environment variable, migrations will run automatically when the server starts. + +To run migrations manually: + +```bash +cd apps/api +npm run migration:run +``` + +## Running in Development Mode + +### Running with Single Command + +To run the API server and web server in development mode: + +```bash +# From project root directory +pnpm dev +``` + +This command starts both the API server and web server simultaneously. The API server runs on port 4000 by default, and the web server runs on port 3000. + +### Running Individual Packages + +#### Building Common Packages + +Before running the web application, you need to build shared packages: + +```bash +# From project root directory +cd packages/ufb-shared +pnpm build +``` + +#### Building UI Packages + +Before running the web application, you need to build UI packages: + +```bash +# From project root directory +cd packages/ufb-tailwindcss +pnpm build +``` + +#### Running Each Server Individually + +To run each server individually: + +```bash +# Run API server only +cd apps/api +pnpm dev + +# Run web server only +cd apps/web +pnpm dev +``` + +## Production Build + +To build the application for production environment: + +```bash +# From project root directory +pnpm build +``` + +This command builds both the API server and web server. + +## Running in Production Mode + +To run the production build: + +```bash +# Run API server +cd apps/api +pnpm start + +# Run web server +cd apps/web +pnpm start +``` + +## API Type Generation + +When the backend API is running, you can generate API types for the frontend: + +```bash +cd apps/web +pnpm generate-api-type +``` + +This command generates TypeScript types from OpenAPI specification and saves them to the `src/shared/types/api.type.ts` file. + +**Note**: For this command to work properly, the API server must be running at `http://localhost:4000`. + +## Code Quality Management + +### Linting + +To run code linting: + +```bash +pnpm lint +``` + +### Formatting + +To run code formatting: + +```bash +pnpm format +``` + +### Testing + +To run tests: + +```bash +pnpm test +``` + +## Swagger Documentation + +When the API server is running, you can check Swagger documentation at the following endpoints: + +- **API Documentation**: http://localhost:4000/docs +- **Admin API Documentation**: http://localhost:4000/admin-docs +- **OpenAPI JSON**: http://localhost:4000/docs-json +- **Admin OpenAPI JSON**: http://localhost:4000/admin-docs-json + +> **Note**: If you are serving the API server on a different URL (e.g., behind a reverse proxy), you can set the `BASE_URL` environment variable to generate correct API endpoint URLs in the Swagger documentation. Example: `BASE_URL=https://api.example.com` + +## Troubleshooting + +### Common Issues + +1. **Dependency Installation Errors**: + - Verify Node.js version is v22.19.0 or higher. + - Verify pnpm version is v10.15.0 or higher. + - Update pnpm to the latest version. + - Try `pnpm install --force`. + +2. **Database Connection Errors**: + - Verify MySQL server is running. + - Verify database credentials are correct. + - Verify `MYSQL_PRIMARY_URL` environment variable format is correct. + - If using Docker infrastructure, verify MySQL is running on port 13306 (not 3306). + +3. **Build Errors**: + - Verify UI packages are built (`pnpm build:ui`). + - Verify all dependencies are installed. + - Check TypeScript errors. + +4. **Runtime Errors**: + - Verify environment variables are set correctly. + - Verify required ports are available. + - Check error messages in logs. diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/04-smtp-configuration.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/04-smtp-configuration.md new file mode 100644 index 000000000..863d721c9 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/04-smtp-configuration.md @@ -0,0 +1,165 @@ +--- +id: smtp-configuration +title: SMTP Server Integration Guide +description: This guide explains how to integrate with external SMTP servers for sending authentication emails in production environments. +sidebar_position: 4 +--- + +# SMTP Server Integration Guide + +In production environments, instead of local test servers like `smtp4dev`, +you need to connect to **external SMTP servers (Gmail, SendGrid, company SMTP, etc.)** +to properly send authentication emails (registration, password reset, etc.). + +This document explains environment variable configuration for SMTP server integration and major integration examples. + +--- + +## 1. SMTP-Related Environment Variables + +Set the following environment variables in the `api` service or `.env` file: + +> **Note**: For SMTP servers that do not require authentication, `SMTP_USERNAME` and `SMTP_PASSWORD` can be omitted. + +| Environment Variable | Description | Required | +| ---------------------------------- | ------------------------------------------------------ | -------- | +| `SMTP_HOST` | SMTP server address (e.g., smtp.gmail.com) | Required | +| `SMTP_PORT` | Port number (usually 587, 465, etc.) | Required | +| `SMTP_SENDER` | Sender email address (e.g., `noreply@yourdomain.com`) | Required | +| `SMTP_USERNAME` | SMTP authentication username (account ID) | Optional | +| `SMTP_PASSWORD` | SMTP authentication password or API key | Optional | +| `SMTP_TLS` | Whether to use TLS (`true` or `false`) | Optional | +| `SMTP_CIPHER_SPEC` | TLS encryption algorithm (default: `TLSv1.2`) | Optional | +| `SMTP_OPPORTUNISTIC_TLS` | Whether to use STARTTLS (`true` or `false`) | Optional | + +> **Important**: In actual code, `SMTP_USERNAME` and `SMTP_PASSWORD` are used, and `SMTP_TLS=true` is mainly used for port 465, while `false` is mainly used for port 587. + +--- + +## 2. Docker Environment Example + +```yaml +api: + image: line/abc-user-feedback-api + environment: + - SMTP_HOST=smtp.gmail.com + - SMTP_PORT=587 + - SMTP_USERNAME=your-email@gmail.com + - SMTP_PASSWORD=your-email-app-password + - SMTP_SENDER=noreply@yourdomain.com + - SMTP_TLS=false + - SMTP_OPPORTUNISTIC_TLS=true +``` + +Or you can manage it separately with a `.env` file: + +```env +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME=your-email@gmail.com +SMTP_PASSWORD=your-email-app-password +SMTP_SENDER=noreply@yourdomain.com +SMTP_TLS=false +SMTP_OPPORTUNISTIC_TLS=true +``` + +--- + +## 3. SMTP Integration Examples + +### ✅ Gmail SMTP Integration (for personal testing) + +- `SMTP_HOST`: `smtp.gmail.com` +- `SMTP_PORT`: `587` +- `SMTP_USERNAME`: Gmail address (e.g., `abc@gmail.com`) +- `SMTP_PASSWORD`: **App password** (Allow less secure apps → not recommended) +- `SMTP_TLS`: `false` +- `SMTP_OPPORTUNISTIC_TLS`: `true` + +> If **two-factor authentication** is enabled on your Gmail account, you must create an [app password](https://myaccount.google.com/apppasswords). + +--- + +### ✅ SendGrid Integration (Recommended) + +- `SMTP_HOST`: `smtp.sendgrid.net` +- `SMTP_PORT`: `587` +- `SMTP_USERNAME`: `apikey` +- `SMTP_PASSWORD`: Actual SendGrid API Key +- `SMTP_SENDER`: verified sender address +- `SMTP_TLS`: `false` +- `SMTP_OPPORTUNISTIC_TLS`: `true` + +--- + +## 4. Testing Methods + +### 4.1 Email Sending Test + +1. **Email Verification Test**: + + - Create admin or user account + - Verify email verification code is sent + +2. **Password Reset Test**: + + - Request password reset + - Verify email with reset link is received + +3. **User Invitation Test**: + - Admin invites new user + - Verify invitation email is sent + +### 4.2 Log Checking + +If email sending fails, check detailed logs with the following command: + +```bash +# Docker Compose environment +docker compose logs api + +# Check logs for specific time period +docker compose logs --since=10m api + +# Real-time log monitoring +docker compose logs -f api +``` + +If SMTP errors occur, detailed messages will be displayed in the logs. + +--- + +## 5. Troubleshooting + +| Problem Type | Cause or Solution | +| ----------------------------- | -------------------------------------- | +| Authentication Error (`535`) | Recheck `SMTP_USERNAME` / `SMTP_PASSWORD` | +| Connection Refused (`ECONNREFUSED`) | Firewall or incorrect port settings | +| Email Not Arriving | `SMTP_SENDER` is not verified | +| TLS Error (`ETLS`) | `SMTP_TLS` setting is incorrect | +| STARTTLS Failure | Check `SMTP_OPPORTUNISTIC_TLS` setting | + +--- + +## 6. Email Templates Related to SMTP + +Currently, emails are sent in the following situations: + +- **Email Verification**: Send verification code when admin/user registers +- **Password Reset**: Send link when password reset is requested +- **User Invitation**: Send invitation email when admin invites user + +Email content is based on **Handlebars templates** and includes the following information: + +- Sender: `"User feedback" ` +- Base URL: Uses `ADMIN_WEB_URL` environment variable value +- Template location: `src/configs/modules/mailer-config/templates/` + +--- + +## Related Documents + +- [Docker Hub Installation Guide](./docker-hub-images) +- [Environment Variable Settings](./configuration) +- [Getting Started Guide](/en/user-guide/getting-started) + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/05-configuration.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/05-configuration.md new file mode 100644 index 000000000..c0aea3aa4 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/05-configuration.md @@ -0,0 +1,159 @@ +--- +id: configuration +title: Environment Variable Configuration +description: This document explains how to configure environment variables for ABC User Feedback's API and web servers. +sidebar_position: 5 +--- + +# Environment Variable Configuration + +This document explains the main environment variables used by ABC User Feedback's **API server** and **web server** and how to configure them. + +--- + +## 1. API Server Environment Variables + +### Required Environment Variables + +| Environment Variable | Description | Default | Example | +| ---------------------------- | ----------------------------- | ------- | -------------------------------- | +| `JWT_SECRET` | Secret key for JWT signing | None | `jwtsecretjwtsecretjwtsecret` | +| `MYSQL_PRIMARY_URL` | MySQL connection URL | None | `mysql://user:pass@host:3306/db` | +| `ACCESS_TOKEN_EXPIRED_TIME` | Access Token validity period | `10m` | `10m`, `30s`, `1h` | +| `REFRESH_TOKEN_EXPIRED_TIME` | Refresh Token validity period | `1h` | `1h`, `7d` | + +> JWT secret should be a sufficiently complex and secure string. + +⚠️ **Security Notes**: + +- `JWT_SECRET` should be at least 32 characters long and complex +- Never use default values in production environments +- Do not include environment variable files (`.env`) in version control +- Manage sensitive information through environment variables or secret management systems + +--- + +### Optional Environment Variables + +| Environment Variable | Description | Default | Example | +| ---------------------- | --------------------------------------------------- | ----------------------- | --------------------------- | +| `APP_PORT` | API server port | `4000` | `4000` | +| `APP_ADDRESS` | Binding address | `0.0.0.0` | `127.0.0.1` | +| `ADMIN_WEB_URL` | Admin web URL | `http://localhost:3000` | `https://admin.company.com` | +| `BASE_URL` | Public API server URL used in Swagger documentation | None | `https://api.example.com` | +| `MYSQL_SECONDARY_URLS` | Secondary DB URLs (JSON array) | None | `["mysql://..."]` | +| `AUTO_MIGRATION` | Auto migration on app startup | `true` | `false` | +| `MASTER_API_KEY` | Master permission API key (optional) | None | `abc123xyz` | +| `NODE_OPTIONS` | Node execution options | None | `--max_old_space_size=4096` | + +--- + +### SMTP Settings (Email Authentication) + +| Environment Variable | Description | Example | +| ------------------------ | ---------------------------- | ------------------------------ | +| `SMTP_HOST` | SMTP server address | `smtp.gmail.com` | +| `SMTP_PORT` | Port (usually 587 or 465) | `587` | +| `SMTP_USERNAME` | Login username | `user@example.com` | +| `SMTP_PASSWORD` | Login password or token | `app-password` | +| `SMTP_SENDER` | Sender address | `noreply@company.com` | +| `SMTP_BASE_URL` | Base URL for links in emails | `https://feedback.company.com` | +| `SMTP_TLS` | Whether to use TLS | `true` | +| `SMTP_CIPHER_SPEC` | Encryption spec | `TLSv1.2` | +| `SMTP_OPPORTUNISTIC_TLS` | Whether to support STARTTLS | `true` | + +📎 For detailed settings, refer to the [SMTP Integration Guide](./04-smtp-configuration.md). + +--- + +## 2. OpenSearch Settings (Optional) + +| Environment Variable | Description | Example | +| --------------------- | ---------------------------- | ----------------------- | +| `OPENSEARCH_USE` | Whether to enable OpenSearch | `true` | +| `OPENSEARCH_NODE` | OpenSearch node URL | `http://localhost:9200` | +| `OPENSEARCH_USERNAME` | Authentication ID | `admin` | +| `OPENSEARCH_PASSWORD` | Authentication password | `admin123` | + +> OpenSearch is used to improve search speed and AI features. + +--- + +## 3. Automatic Feedback Deletion Settings + +| Environment Variable | Description | Default / Condition | +| ------------------------------------ | ------------------------------------ | --------------------------- | +| `AUTO_FEEDBACK_DELETION_ENABLED` | Enable old feedback deletion feature | `false` | +| `AUTO_FEEDBACK_DELETION_PERIOD_DAYS` | Deletion criteria in days | `365` (required if enabled) | + +--- + +## 4. Web Server Environment Variables + +### Required Environment Variables + +| Environment Variable | Description | Example | +| -------------------------- | --------------------------------- | ----------------------- | +| `NEXT_PUBLIC_API_BASE_URL` | API server address for client use | `http://localhost:4000` | + +### Optional Environment Variables + +| Environment Variable | Description | Default | Example | +| -------------------- | ------------- | ------- | ------- | +| `PORT` | Frontend port | `3000` | `3000` | + +--- + +## 5. Configuration Methods + +### Docker Compose Example + +```yaml +services: + api: + image: line/abc-user-feedback-api + environment: + - JWT_SECRET=changeme + - MYSQL_PRIMARY_URL=mysql://user:pass@mysql:3306/userfeedback + - SMTP_HOST=smtp.sendgrid.net + - SMTP_USERNAME=apikey + - SMTP_PASSWORD=your-sendgrid-key +``` + +### .env File Example + +``` +# apps/api/.env +JWT_SECRET=changemechangemechangeme +MYSQL_PRIMARY_URL=mysql://root:pass@localhost:3306/db +ACCESS_TOKEN_EXPIRED_TIME=10m +REFRESH_TOKEN_EXPIRED_TIME=1h +SMTP_HOST=smtp.example.com +SMTP_SENDER=noreply@example.com +# BASE_URL=https://api.example.com # Set when serving behind a reverse proxy + +# apps/web/.env +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +``` + +--- + +## 7. Troubleshooting Guide + +| Problem | Cause and Solution | +| --------------------------------------- | --------------------------------------------------- | +| Environment variables not recognized | Check `.env` location or restart container | +| DB connection failure | Check `MYSQL_PRIMARY_URL` format or connection info | +| SMTP error | Recheck port/TLS settings or authentication info | +| OpenSearch error | Check node URL or user authentication | +| JWT token error | Check `JWT_SECRET` length and complexity | +| Environment variable validation failure | Check for missing required variables or type errors | +| Port conflict | Check `APP_PORT`, `PORT` settings | + +--- + +## Related Documents + +- [Docker Installation Guide](./docker-hub-images) +- [SMTP Integration Guide](./smtp-configuration) +- [Getting Started Guide](/en/user-guide/getting-started) diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/_category_.json b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/_category_.json new file mode 100644 index 000000000..fab355450 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 1, + "label": "Installation", + "description": "Guide for setting up development environment and installation." +} diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/index.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/index.md new file mode 100644 index 000000000..7db09e9de --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/index.md @@ -0,0 +1,7 @@ +--- +title: Installation +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/02-api-integration.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/02-api-integration.md new file mode 100644 index 000000000..d4e1b207a --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/02-api-integration.md @@ -0,0 +1,533 @@ +--- +sidebar_position: 2 +title: "API Integration" +description: "This guide explains how to integrate external systems using ABC User Feedback API and provides actual implementation examples." +--- + +# API Integration + +ABC User Feedback can integrate with external systems through **RESTful API**. You can programmatically collect feedback, manage issues, and query data, making it easy to integrate with existing services or workflows. + +--- + +## API Basic Information + +### Official API Documentation + +The **complete API documentation** for ABC User Feedback can be found at the following link: + +🔗 **[Official API Documentation (Redocly)](https://line.github.io/abc-user-feedback/)** + +This documentation provides detailed specs for all endpoints, request/response examples, and an interface for actual testing. + +### Base URL + +``` +https://your-domain.com/api +``` + +### Authentication Method + +All API requests use **API key-based authentication**. + +```http +X-API-KEY: your-api-key-here +Content-Type: application/json +``` + +:::warning Security Notice +Use API keys only on the server side, and do not expose them to clients (browsers, mobile apps). +::: + +### API Key Issuance Method + +1. **Access Admin Page**: Log in to ABC User Feedback admin page +2. **Project Settings**: Navigate to the settings page for the project +3. **API Key Management**: Create a new API key from the "API Key Management" menu +4. **Copy Key**: Save the generated API key in a safe place + +:::info API Key Permissions +API keys are issued per project and can only access data for that project. +::: + +--- + +## Main API Endpoint Examples + +### 1. Creating Feedback + +#### Basic Feedback Creation + +```javascript +const createFeedback = async ( + projectId, + channelId, + message, + issueNames = [] +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + message: message, + issueNames: issueNames, + }), + } + ); + + return await response.json(); +}; + +// Usage example +const feedback = await createFeedback(1, 1, "Payment error occurred", [ + "Payment", + "Error", +]); +``` + +### 2. Querying Feedback + +#### Channel-Based Feedback Search + +```javascript +const searchFeedbacks = async ( + projectId, + channelId, + searchText, + limit = 10, + page = 1 +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/search`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + limit: limit, + page: page, + query: { + searchText: searchText, + createdAt: { + gte: "2024-01-01", + lt: "2024-12-31", + }, + }, + sort: { + createdAt: "DESC", + }, + }), + } + ); + + return await response.json(); +}; + +// Usage example +const feedbacks = await searchFeedbacks(1, 1, "Payment", 20, 1); +console.log( + `Retrieved ${feedbacks.items.length} out of ${feedbacks.meta.totalItems} feedback` +); +``` + +#### Single Feedback Query + +```javascript +const getFeedbackById = async (projectId, channelId, feedbackId) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, + { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// Usage example +const feedback = await getFeedbackById(1, 1, 123); +console.log("Feedback details:", feedback); +``` + +#### Feedback Update + +```javascript +const updateFeedback = async (projectId, channelId, feedbackId, updateData) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify(updateData), + } + ); + + return await response.json(); +}; + +// Usage example +const updatedFeedback = await updateFeedback(1, 1, 123, { + message: "Updated feedback content", + issueNames: ["Updated issue"], +}); +``` + +#### Feedback Deletion + +```javascript +const deleteFeedbacks = async (projectId, channelId, feedbackIds) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + feedbackIds: feedbackIds, + }), + } + ); + + return await response.json(); +}; + +// Usage example +const result = await deleteFeedbacks(1, 1, [123, 124, 125]); +console.log("Deletion complete:", result); +``` + +### 3. Issue Management + +#### Issue Creation + +```javascript +const createIssue = async (projectId, name, description) => { + const response = await fetch(`/api/projects/${projectId}/issues`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + name: name, + description: description, + }), + }); + + return await response.json(); +}; + +// Usage example +const issue = await createIssue( + 1, + "Payment Error", + "User experienced error during payment process" +); +``` + +#### Issue Search + +```javascript +const searchIssues = async (projectId, query = {}) => { + const response = await fetch(`/api/projects/${projectId}/issues/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + limit: 10, + page: 1, + query: query, + sort: { + createdAt: "DESC", + }, + }), + }); + + return await response.json(); +}; + +// Usage example +const issues = await searchIssues(1, { name: "Payment" }); +``` + +#### Issue Query + +```javascript +const getIssueById = async (projectId, issueId) => { + const response = await fetch(`/api/projects/${projectId}/issues/${issueId}`, { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + }); + + return await response.json(); +}; + +// Usage example +const issue = await getIssueById(1, 123); +console.log("Issue details:", issue); +``` + +#### Issue Update + +```javascript +const updateIssue = async (projectId, issueId, updateData) => { + const response = await fetch(`/api/projects/${projectId}/issues/${issueId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify(updateData), + }); + + return await response.json(); +}; + +// Usage example +const updatedIssue = await updateIssue(1, 123, { + name: "Updated issue name", + description: "Updated issue description", +}); +``` + +#### Issue Deletion + +```javascript +const deleteIssues = async (projectId, issueIds) => { + const response = await fetch(`/api/projects/${projectId}/issues`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + issueIds: issueIds, + }), + }); + + return await response.json(); +}; + +// Usage example +const result = await deleteIssues(1, [123, 124, 125]); +console.log("Issue deletion complete:", result); +``` + +#### Adding Issue to Feedback + +```javascript +const addIssueToFeedback = async ( + projectId, + channelId, + feedbackId, + issueId +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}/issues/${issueId}`, + { + method: "POST", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// Usage example +const result = await addIssueToFeedback(1, 1, 123, 456); +console.log("Issue added:", result); +``` + +#### Removing Issue from Feedback + +```javascript +const removeIssueFromFeedback = async ( + projectId, + channelId, + feedbackId, + issueId +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}/issues/${issueId}`, + { + method: "DELETE", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// Usage example +const result = await removeIssueFromFeedback(1, 1, 123, 456); +console.log("Issue removed:", result); +``` + +### 4. Project and Channel Information + +#### Project Information Query + +```javascript +const getProjectInfo = async (projectId) => { + const response = await fetch(`/api/projects/${projectId}`, { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + }); + + return await response.json(); +}; + +// Usage example +const project = await getProjectInfo(1); +console.log("Project information:", project); +``` + +#### Channel Field Query + +```javascript +const getChannelFields = async (projectId, channelId) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/fields`, + { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// Usage example +const fields = await getChannelFields(1, 1); +console.log("Channel fields:", fields); +``` + +--- + +## API Testing via Swagger + +ABC User Feedback provides **Swagger UI** to easily test and understand the API. + +### Swagger Access Method + +Access via **API server address + `/docs`**: + +``` +https://your-domain.com/api/docs +``` + +Or in **ReDoc format**: + +``` +https://your-domain.com/api/docs/redoc +``` + +### Setting API Key in Swagger + +1. Click the **"Authorize"** button at the top of Swagger UI +2. Enter the issued API key in the **X-API-KEY** field +3. Click **"Authorize"** to complete authentication + +After this, all API requests will automatically include the API key for testing. + +### Swagger Usage Tips + +- Use **"Try it out"** button to test actual API calls +- Check actual response data structure in **Response body** section +- View detailed request/response data format in **Schema** tab +- Generate **cURL** commands automatically for CLI testing + +--- + +## Error Handling and Retry Logic + +### HTTP Status Codes + +| Status Code | Meaning | Handling Method | +| ----------- | ---------------- | ----------------------------- | +| **200** | Success | Normal processing | +| **400** | Bad Request | Validate request data | +| **401** | Authentication Failed | Check API key | +| **403** | Forbidden | Check project access permissions | +| **404** | Not Found | Check ID value | +| **429** | Rate Limit Exceeded | Retry after a moment | +| **500** | Server Error | Retry or contact support team | + +## Response Data Parsing Method + +### Pagination Response Structure + +```json +{ + "meta": { + "itemCount": 10, + "totalItems": 100, + "itemsPerPage": 10, + "totalPages": 10, + "currentPage": 1 + }, + "items": [ + { + "id": 1, + "message": "Feedback content", + "createdAt": "2024-01-01T00:00:00.000Z", + "issues": [ + { + "id": 1, + "name": "Issue name" + } + ] + } + ] +} +``` + +## Security and Performance Optimization + +### API Key Security + +- **Use environment variables**: Manage API keys as environment variables +- **Server side only**: Do not expose API keys to clients +- **Key rotation**: Regularly replace API keys +- **IP whitelist**: Allow access only from specific IPs when possible + +### Performance Optimization + +- **Use pagination**: Set appropriate limit when querying large amounts of data +- **Request only needed fields**: Improve response speed through query optimization +- **Caching strategy**: Cache frequently queried data on client side +- **Batch processing**: Process multiple requests together + +## Related Documents + +- [API Key Management](/en/user-guide/settings/api-key-management) - How to issue API keys from UI +- [Image Settings](/en/user-guide/settings/image-setting) - Settings for using image upload API +- [Webhook Integration](/en/user-guide/settings/webhook-management) - Real-time notification settings that can be used with API + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/03-oauth-integration.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/03-oauth-integration.md new file mode 100644 index 000000000..f865a29ac --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/03-oauth-integration.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 3 +title: "OAuth Integration" +description: "This guide explains how to integrate single sign-on (SSO) through Google OAuth and custom OAuth providers." +--- + +# OAuth Integration + +By setting up OAuth 2.0-based single sign-on (SSO) in ABC User Feedback, users can log in with existing accounts (Google, Microsoft, GitHub, etc.) without creating separate accounts. This improves user convenience and is essential for implementing integrated authentication in enterprise environments. + +--- + +## OAuth Integration Overview + +OAuth methods supported by ABC User Feedback: + +### 1. Google OAuth + +- Provided by default without additional setup +- Easy login via Google account + +### 2. Custom OAuth Provider + +- In-house authentication systems +- Other OAuth 2.0/OpenID Connect compatible services + +After setting up OAuth, it can be used alongside existing email login, and you can restrict to OAuth only according to organizational policy. + +--- + +## Google OAuth Integration Settings + +### Settings in Google Cloud Console + +#### 1. Access Google Cloud Console + +Access [Google Cloud Console](https://console.cloud.google.com) and create a project or select an existing project. + +#### 2. Create OAuth 2.0 Client ID + +1. Navigate to **APIs & Services > Credentials** menu +2. Select **+ Create Credentials > OAuth Client ID** +3. Select application type as **Web Application** + +#### 3. Set Authorized Redirect URIs + +Add the following URL to **Authorized Redirect URIs**: + +``` +https://your-domain.com/auth/oauth-callback +``` + +Examples: + +- `https://feedback.company.com/auth/oauth-callback` +- `http://localhost:3000/auth/oauth-callback` (development environment) + +#### 4. Check Client Information + +After creation is complete, check and copy the following information: + +- **Client ID**: `1234567890-abc123def456.apps.googleusercontent.com` +- **Client Secret**: `GOCSPX-abcdef123456` + +### Google OAuth Settings in ABC User Feedback + +To use Google OAuth, follow these steps to configure: + +#### 1. Enable Google OAuth Settings + +In **Settings > Login Management**: + +1. Enable **OAuth2.0 Login** toggle +2. Select **Login Button Type** as "Google Login" +3. Enter information obtained from Google Cloud Console: + - **Client ID**: Client ID created in Google Cloud Console + - **Client Secret**: Client secret created in Google Cloud Console + - **Authorization Code Request URL**: `https://accounts.google.com/o/oauth2/v2/auth` + - **Scope**: `openid email profile` + - **Access Token URL**: `https://oauth2.googleapis.com/token` + - **User Profile Request URL**: `https://www.googleapis.com/oauth2/v2/userinfo` + - **Email Key**: `email` + +#### 2. Register Redirect URI + +Add the following URL to **Authorized Redirect URIs** in Google Cloud Console: + +``` +https://your-domain.com/auth/oauth-callback +``` + +For development environment: + +``` +http://localhost:3000/auth/oauth-callback +``` + +--- + +## Custom OAuth Provider Integration + +### In-House Authentication System Integration + +ABC User Feedback can integrate with in-house authentication systems used in enterprise environments. Most in-house authentication systems support OAuth 2.0 or OpenID Connect standards, so integration is possible through standard OAuth flow. + +#### Requirements for In-House Authentication System Settings + +To integrate with an in-house authentication system, the following information is required: + +1. **OAuth Client Registration** + + - Client ID + - Client Secret + - Redirect URI: `https://your-domain.com/auth/oauth-callback` + +2. **OAuth Endpoint Information** + + - Authorization URL (authentication request URL) + - Token URL (token exchange URL) + - User Info URL (user information query URL) + +3. **Permission Scope (Scope)** + - User profile information access permissions + - Email address access permissions + +#### Common In-House Authentication System Examples + +| Item | Description | In-House System Example | +| ---------------------------------- | ----------------------------------------- | ------------------------------------------------- | +| **Login Button Type** | Login button type | `CUSTOM` | +| **Login Button Name** | Name displayed on login button | `Sign in with Company Account` | +| **Client ID** | OAuth client ID | `company-auth-client-123` | +| **Client Secret** | Client secret | `company-secret-abc123` | +| **Authorization Code Request URL** | User authentication request URL | `https://auth.company.com/oauth/authorize` | +| **Scope** | Permission scope to request | `openid email profile` | +| **Access Token URL** | Token request URL | `https://auth.company.com/oauth/token` | +| **User Profile Request URL** | User information query API | `https://auth.company.com/api/user` | +| **Email Key** | Email field name in user information JSON | `email` or `mail` | + +### Other OAuth 2.0/OpenID Connect Compatible Services + +ABC User Feedback can integrate with all authentication services that comply with OAuth 2.0 or OpenID Connect standards. + +#### Supported Service Types + +- **OpenID Connect Providers**: Services supporting standard OpenID Connect protocol +- **OAuth 2.0 Providers**: Services supporting OAuth 2.0 Authorization Code flow +- **Custom Authentication Servers**: Self-built services providing standard OAuth endpoints + +#### Integration Setup Method + +Configure custom OAuth in **Settings > Login Management**: + +1. **Log in with admin account** and navigate to **Settings > Login Management** menu +2. Enable **OAuth2.0 Login** toggle +3. Select **Login Button Type** as `CUSTOM` +4. Enter information received from authentication service provider: + - **Login Button Name**: Text displayed on login button (e.g., "Sign in with Company Account") + - **Client ID**: OAuth client identifier + - **Client Secret**: Client authentication secret + - **Authorization Code Request URL**: User authentication request URL + - **Scope**: Permission scope to request (space-separated, e.g., "openid email profile") + - **Access Token URL**: Access token request URL + - **User Profile Request URL**: User profile information query URL + - **Email Key**: Email field name in user information JSON (e.g., "email" or "mail") + +--- + +## Related Documents + +- [Login Management](/en/user-guide/settings/tenant-settings) - How to configure OAuth in UI + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/04-webhook-integration.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/04-webhook-integration.md new file mode 100644 index 000000000..c198442c7 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/04-webhook-integration.md @@ -0,0 +1,303 @@ +--- +sidebar_position: 4 +title: 'Webhook Integration' +description: 'This guide explains how to integrate with external systems in real-time using webhooks and provides implementation examples.' +--- + +# Webhook Integration + +With webhooks, you can deliver major events occurring in ABC User Feedback to external systems in real-time. You can integrate with Slack notifications, automation workflows, custom analysis systems, etc. + +--- + +## Supported Event Types + +Events supported by ABC User Feedback are as follows: + +### 1. FEEDBACK_CREATION + +Occurs when new feedback is created. + +**Request Headers:** + +``` +Content-Type: application/json +x-webhook-token: your-secret-token +``` + +**Payload Example:** + +```json +{ + "event": "FEEDBACK_CREATION", + "data": { + "feedback": { + "id": 123, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "message": "User feedback content", + "userEmail": "user@example.com", + "issues": [ + { + "id": 456, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "name": "Bug Report", + "description": "Issue description", + "status": "OPEN", + "externalIssueId": "EXT-123", + "feedbackCount": 5 + } + ] + }, + "channel": { + "id": 1, + "name": "Website Feedback" + }, + "project": { + "id": 1, + "name": "My Project" + } + } +} +``` + +### 2. ISSUE_CREATION + +Occurs when a new issue is created. + +**Payload Example:** + +```json +{ + "event": "ISSUE_CREATION", + "data": { + "issue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T11:00:00.000Z", + "name": "New Issue", + "description": "Issue description", + "status": "OPEN", + "externalIssueId": "EXT-789", + "feedbackCount": 0 + }, + "project": { + "id": 1, + "name": "My Project" + } + } +} +``` + +### 3. ISSUE_STATUS_CHANGE + +Occurs when issue status is changed. + +**Payload Example:** + +```json +{ + "event": "ISSUE_STATUS_CHANGE", + "data": { + "issue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T12:00:00.000Z", + "name": "Issue Name", + "description": "Issue description", + "status": "IN_PROGRESS", + "externalIssueId": "EXT-789", + "feedbackCount": 3 + }, + "project": { + "id": 1, + "name": "My Project" + }, + "previousStatus": "OPEN" + } +} +``` + +### 4. ISSUE_ADDITION + +Occurs when an issue is added to feedback. + +**Payload Example:** + +```json +{ + "event": "ISSUE_ADDITION", + "data": { + "feedback": { + "id": 123, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "message": "User feedback content", + "issues": [ + { + "id": 456, + "name": "Existing Issue", + "status": "OPEN" + }, + { + "id": 789, + "name": "Newly Added Issue", + "status": "OPEN" + } + ] + }, + "channel": { + "id": 1, + "name": "Website Feedback" + }, + "project": { + "id": 1, + "name": "My Project" + }, + "addedIssue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T11:00:00.000Z", + "name": "Newly Added Issue", + "description": "Issue description", + "status": "OPEN", + "externalIssueId": "EXT-456", + "feedbackCount": 1 + } + } +} +``` + +--- + +## Webhook Receiving Server Implementation + +You need to implement an HTTP server to receive webhooks. The server must meet the following requirements: + +### Basic Requirements + +1. **HTTP POST Request Handling**: Webhooks are sent via HTTP POST +2. **JSON Payload Parsing**: Request body is in JSON format +3. **Return 200 Response Code**: Must respond with 200 status code on successful processing + +### Implementation Example (Node.js/Express) + +```javascript +const express = require('express'); +const app = express(); + +app.use(express.json()); + +app.post('/webhook', (req, res) => { + const { event, data } = req.body; + const token = req.headers['x-webhook-token']; + + // Token verification + if (token !== 'your-secret-token') { + return res.status(401).json({ error: 'Unauthorized' }); + } + + // Event processing + switch (event) { + case 'FEEDBACK_CREATION': + console.log('New feedback created:', data.feedback); + // Feedback processing logic + break; + case 'ISSUE_CREATION': + console.log('New issue created:', data.issue); + // Issue processing logic + break; + case 'ISSUE_STATUS_CHANGE': + console.log( + 'Issue status changed:', + data.issue, + 'Previous status:', + data.previousStatus, + ); + // Status change processing logic + break; + case 'ISSUE_ADDITION': + console.log('Issue added:', data.addedIssue); + // Issue addition processing logic + break; + } + + res.status(200).json({ success: true }); +}); + +app.listen(3000, () => { + console.log('Webhook listener server running on port 3000.'); +}); +``` + +--- + +## Security and Retry Policy + +### Security Considerations + +- **Token Verification**: Verify requests through `x-webhook-token` header +- **HTTPS Usage**: Always use HTTPS in production environments + +### Retry Policy + +- **Automatic Retry**: ABC User Feedback automatically retries up to 3 times on webhook transmission failure +- **Retry Interval**: Each retry is executed after 3 seconds + +### Error Handling + +- **4xx Errors**: Considered client errors, not retried +- **5xx Errors**: Considered server errors, retried +- **Network Errors**: Retried on connection failure + +--- + +## Usage Examples + +### 1. Automatic Translation + +```javascript +// Receive FEEDBACK_CREATION event and automatically translate +if (event === 'FEEDBACK_CREATION') { + const translatedMessage = await translateText(data.feedback.message); + // Update feedback with translated content + await updateFeedback(data.feedback.id, { translatedMessage }); +} +``` + +### 2. External Ticket System Integration + +```javascript +// Receive ISSUE_CREATION event and create ticket in external system +if (event === 'ISSUE_CREATION') { + const ticketId = await createExternalTicket({ + title: data.issue.name, + description: data.issue.description, + priority: 'medium', + }); + // Store external ticket ID in issue + await updateIssue(data.issue.id, { externalIssueId: ticketId }); +} +``` + +### 3. Notification System Integration + +```javascript +// Receive ISSUE_STATUS_CHANGE event and notify team +if (event === 'ISSUE_STATUS_CHANGE') { + await sendSlackNotification({ + channel: '#feedback-alerts', + message: `Issue "${data.issue.name}" status changed from ${data.previousStatus} to ${data.issue.status}.`, + }); +} +``` + +--- + +## Related Documents + +- [Webhook Management](/en/user-guide/settings/webhook-management) - How to configure webhooks in UI +- [API Integration](./02-api-integration.md) - API usage that can be used with webhooks +- [Issue Management](/en/user-guide/issue-management) - Understanding issue status change events + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/_category_.json b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/_category_.json new file mode 100644 index 000000000..1e4297079 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 3, + "label": "Developer Guide" +} + diff --git a/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/index.md b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/index.md new file mode 100644 index 000000000..c82e1dd8d --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-plugin-content-docs/current/02-developer-guide/index.md @@ -0,0 +1,7 @@ +--- +title: Developer Guide +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/en/docusaurus-theme-classic/footer.json b/apps/docs/i18n/en/docusaurus-theme-classic/footer.json new file mode 100644 index 000000000..5f7733efa --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-theme-classic/footer.json @@ -0,0 +1,14 @@ +{ + "copyright": { + "message": "Copyright © {year} ABC User Feedback.", + "description": "The footer copyright" + }, + "link.title.Docs": { + "message": "Docs", + "description": "The title of the footer links column with title=Docs in the footer" + }, + "link.title.More": { + "message": "More", + "description": "The title of the footer links column with title=More in the footer" + } +} diff --git a/apps/docs/i18n/en/docusaurus-theme-classic/navbar.json b/apps/docs/i18n/en/docusaurus-theme-classic/navbar.json new file mode 100644 index 000000000..b76181384 --- /dev/null +++ b/apps/docs/i18n/en/docusaurus-theme-classic/navbar.json @@ -0,0 +1,18 @@ +{ + "title": { + "message": "ABC User Feedback", + "description": "The title in the navbar" + }, + "item.label.Docs": { + "message": "Docs", + "description": "Navbar item with label Docs" + }, + "item.label.GitHub": { + "message": "GitHub", + "description": "Navbar item with label GitHub" + }, + "logo.alt": { + "message": "LOGO", + "description": "The alt text of navbar logo" + } +} diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current.json b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current.json new file mode 100644 index 000000000..b67c64072 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,27 @@ +{ + "version.label": { + "message": "Next", + "description": "The label for version current" + }, + "sidebar.docs.category.소개": { + "message": "紹介", + "description": "The label for category Introduction in sidebar docs" + }, + "sidebar.docs.category.사용자 가이드": { + "message": "ユーザーガイド", + "description": "The label for category User Guide in sidebar docs" + }, + "sidebar.docs.category.설정": { + "message": "設定", + "description": "The label for category Settings in sidebar docs" + }, + "sidebar.docs.category.개발자 가이드": { + "message": "開発者ガイド", + "description": "The label for category Developer Guide in sidebar docs" + }, + "sidebar.docs.category.설치": { + "message": "インストール", + "description": "The label for category Installation in sidebar docs" + } +} + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/00-index.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/00-index.md new file mode 100644 index 000000000..61bbcc890 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/00-index.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 1 +slug: / +--- + +# ようこそ + +ようこそ!このドキュメントは、ABC User Feedbackに関する包括的なガイドを提供します。 + +![ABC User Feedback](/assets/cover.png) + +## ABC User Feedbackとは? + +ABC User Feedbackは、顧客の声(VoC)フィードバックを効率的に収集、分類、管理するために設計されたスタンドアロンのWebアプリケーションです。フィードバックタギングシステム、カンバンモード、イシュートラッカー、SSO認証など、さまざまな機能を提供します。現在、1,000万MAUを有するサービスで利用されています。 + +

+

+ +## 主な機能 + +- **フィードバックタギングシステム**: トピック別にフィードバックを分類・管理 +- **カンバンモード**: イシューグループを効率的に視覚化・管理 +- **イシュートラッカー統合**: ステータスインジケーターでイシューを追跡し、外部システムと統合 +- **シングルサインオン(SSO)**: エンタープライズレベルの認証要件をサポートするOAuth認証 +- **ロールベースアクセス制御(RBAC)**: きめ細かいユーザー権限管理 +- **ダッシュボード**: フィードバックとイシューの統計データを視覚化 + +## はじめに + +- [インストールガイド](/ja/developer-guide/installation/docker-hub-images) - Docker、CLIツール、または手動セットアップによるインストール方法 +- [チュートリアル](/ja/user-guide/getting-started) - 基本的な使用方法ガイド + +## サポートを受ける + +質問がある、またはヘルプが必要ですか?以下のリソースをご利用ください: + +- [GitHub Issues](https://github.com/line/abc-user-feedback/issues) - バグレポートと機能リクエスト +- [GitHub Discussions](https://github.com/line/abc-user-feedback/discussions) - コミュニティディスカッション + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/01-project-overview.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/01-project-overview.md new file mode 100644 index 000000000..7ad10db0b --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/01-project-overview.md @@ -0,0 +1,75 @@ +--- +sidebar_position: 1 +title: "プロジェクト概要" +description: "プロジェクト概要を紹介します。" +--- + +# プロジェクト概要 + +## ABC User Feedbackとは? + +ABC User Feedbackは、顧客の声(Voice of Customer、VoC)を効率的に収集、分類、管理するために設計されたスタンドアロンのWebアプリケーションです。このオープンソースソリューションは、ユーザーフィードバックを体系的に管理し、製品とサービスの改善に必要なインサイトを導き出すことに重点を置いています。 + +現在、このアプリケーションは月間アクティブユーザー(MAU)1,000万人規模のサービスで利用されており、大規模なフィードバック処理に対する実証済みの安定性を備えています。 + +## コアバリュー提案 + +ABC User Feedbackは、以下のコアバリューを提供します: + +1. **中央集約型フィードバック管理**: さまざまなチャネルから収集されたユーザーフィードバックを一箇所で管理 +2. **構造化された分析**: イシューシステムを通じたフィードバックの分類とトレンドの把握 +3. **イシュー追跡**: フィードバックで発見された問題点をイシューに変換して追跡管理 +4. **データ駆動型意思決定**: ダッシュボードを通じたフィードバックデータの視覚化とインサイトの導出 + +## 技術スタック + +ABC User Feedbackは、最新のWeb技術に基づいて構築されています: + +- **フロントエンド**: [Next.js](https://nextjs.org/) - Reactベースのフロントエンドフレームワーク +- **バックエンド**: [NestJS](https://nestjs.com/) - TypeScriptベースのスケーラブルなバックエンドフレームワーク +- **データベース**: [MySQL v8](https://www.mysql.com/) - 信頼性の高いリレーショナルデータベース +- **検索エンジン**: [OpenSearch v2.16](https://opensearch.org/)(オプション) - 大量のフィードバックデータに対する高性能検索機能 + +## アーキテクチャ概要 + +ABC User Feedbackは、以下の主要コンポーネントで構成されています: + +1. **Web管理インターフェース**: フィードバック管理、イシュー追跡、ダッシュボードなどのユーザーインターフェースを提供するNext.jsベースのWebアプリケーション +2. **APIサーバー**: データ処理、ビジネスロジック、認証などを担当するNestJSベースのバックエンドサーバー +3. **データベース**: フィードバック、イシュー、ユーザー情報などを保存するMySQLデータベース +4. **検索エンジン**: 大量のフィードバックデータに対する高性能検索を提供するOpenSearch(オプション) +5. **SMTPサーバー**: アカウント作成時のメール認証、パスワードリセットなど、ユーザー認証プロセスに必要なメール送信を担当するコンポーネント + +これらのコンポーネントは、Dockerを通じてコンテナ化されており、簡単にデプロイおよびスケールできます。 + +## 主な使用例 + +ABC User Feedbackは、以下の状況で特に有用です: + +1. **製品改善プロセス**: ユーザーフィードバックを収集・分析して製品改善の方向性を設定 +2. **カスタマーサポート**: ユーザーの問い合わせとイシューを効率的に追跡・管理 +3. **ユーザー体験の最適化**: ユーザーの意見に基づいてUX/UIを改善 +4. **品質管理**: バグレポートと機能リクエストを体系的に管理 +5. **データ駆動型意思決定**: ユーザーフィードバック統計を活用した戦略的意思決定のサポート + +## 差別化要素 + +ABC User Feedbackは、以下の特徴により、他のフィードバック管理ツールと差別化されています: + +1. **完全なオープンソース**: 商用ソリューションとは異なり、完全に無料で使用でき、カスタマイズ可能 +2. **エンタープライズレベルの機能**: SSO認証、RBACなど、企業環境に必要な機能を提供 +3. **スケーラビリティ**: 大規模なユーザーベース(1,000万MAU)で実証されたパフォーマンス +4. **統合の容易さ**: RESTful APIとWebhookによる既存システムとの簡単な統合 +5. **コンテナ化**: Dockerサポートによる簡単なデプロイとスケーリング + +## 次のステップ + +ABC User Feedbackを始めるには、以下のドキュメントを参照してください: + +- [主な機能](./02-key-features.md) - 詳細な機能説明 +- [インストールガイド](/ja/developer-guide/installation/docker-hub-images) - インストール方法 + +--- + +このドキュメントは、ABC User Feedbackの基本的な概要を提供します。より詳細な情報については、該当セクションのドキュメントを参照してください。 + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/02-key-features.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/02-key-features.md new file mode 100644 index 000000000..7ef780a12 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/02-key-features.md @@ -0,0 +1,173 @@ +--- +sidebar_position: 3 +title: '主な機能' +description: '主な機能の紹介。' +--- + +# 主な機能 + +ABC User Feedbackは、ユーザーフィードバックを効果的に収集、管理、分析するための様々な機能を提供します。このドキュメントでは、コア機能について詳しく説明します。 + +## フィードバックタグシステム + +![フィードバックタグ](/assets/01-feedback-tag.png) + +フィードバックタグシステムは、大量のユーザーフィードバックを体系的に分類・管理するためのコア機能です。 + +### 主な機能 + +- **複数イシューの割り当て**:各フィードバックに複数のイシューを割り当てて多次元的に分類 +- **カスタムイシューの作成**:プロジェクトの特性に合わせたカスタマイズされたイシューの作成と管理 +- **イシュー別フィルタリング**:イシュー別にフィードバックをフィルタリングして特定のトピックに集中 +- **イシュー統計**:イシューの使用頻度とトレンド分析による洞察の導出 + +### 使用方法 + +1. 管理パネルでイシューカテゴリとイシューを作成 +2. 受信したフィードバックに関連イシューを割り当て +3. イシュー別にフィードバックをフィルタリングして分析 +4. イシューの使用パターンから主要なイシューとトレンドを特定 + +## カンバンモード + +![イシューカンバン](/assets/02-Issue-Kanban.png) + +カンバンモードは、イシューグループを視覚的に管理し、ワークフローを最適化するための機能です。 + +### 主な機能 + +- **直感的なドラッグ&ドロップ**:イシューのステータス変更のためのシンプルなインターフェース +- **ステータスベースのカラム構成**:イシューの進行状況に基づくカラム分離(例:To Do、In Progress、Done) +- **ワークフローの可視化**:チームの作業プロセスと進捗を一目で把握 +- **作業負荷管理**:各ステータスのイシュー数を通じた作業負荷のモニタリング +- **フィルタとソート**:カンバンボード内のイシューを様々な基準でフィルタリング・ソート + +### 使用方法 + +1. カンバンモードビューを選択 +2. ステータス別にイシューを確認・管理 +3. ドラッグ&ドロップでイシューのステータスを変更 +4. チームのワークフローを最適化し、ボトルネックを特定 + +## イシュートラッカー連携 + +![イシュートラッカー](/assets/03-issue-tracker.png) + +イシュートラッカー連携は、フィードバックで発見された問題や改善事項を体系的に管理するための機能です。 + +### 主な機能 + +- **ステータスインジケーター**:イシューの現在のステータスを視覚的に表示(New、In Progress、Resolvedなど) +- **外部システム連携**:イシュートラッカーシステム(JIRA)との接続 + +### 使用方法 + +1. フィードバックからイシューを作成、またはイシューメニューで作成 +2. 外部イシュートラッカー接続を設定(オプション) +3. イシューの詳細とイシュー追跡チケットを設定 +4. イシューの進捗をモニタリング・更新 +5. 解決後にイシューをクローズ + +## シングルサインオン(SSO) + +![シングルサインオン](/assets/04-single-signon.png) + +シングルサインオンは、企業環境での認証プロセスを簡素化し、セキュリティを強化します。 + +### 主な機能 + +- **OAuthサポート**:様々なOAuthプロバイダーを通じた認証サポート +- **企業IDとの統合**:既存の企業IDシステムとのシームレスな統合 +- **一元化されたユーザー管理**:単一の認証システムを通じたユーザーアクセス管理 +- **セキュリティの強化**:多要素認証と企業セキュリティポリシーの適用 +- **簡素化されたログイン体験**:ユーザーが追加アカウントを作成する必要がない + +### サポートされるSSOプロバイダー + +- Google +- カスタム(標準的なOAuth 2.0およびOpenID Connectプロバイダー) + +### 使用方法 + +1. 管理設定でSSOプロバイダーを設定 +2. 認証パラメータとリダイレクトURLを設定 +3. ユーザー属性マッピングを設定 +4. SSOログインを有効化してテスト + +## ロールベースアクセス制御(RBAC) + +![ロール管理](/assets/05-role-management.png) + +ロールベースアクセス制御は、ユーザー権限を効果的に管理し、システムセキュリティを維持するための機能です。 + +### 主な機能 + +- **事前定義されたロール**:管理者、アナリスト、閲覧者などの基本ロールを提供 +- **カスタムロールの作成**:組織構造に合わせたカスタマイズされたロールと権限の作成 +- **詳細な権限制御**:機能とデータ別にアクセス権限を設定 +- **ロール割り当て管理**:ユーザーごとにロールを割り当て・変更 +- **権限の継承**:階層的な権限構造をサポート + +### 使用方法 + +1. 管理パネルでロール管理メニューにアクセス +2. 必要に応じて新しいロールを作成、または既存のロールを修正 +3. ユーザーに適切なロールを割り当て +4. ロール別の権限とアクセス範囲を定期的にレビュー + +## ダッシュボード + +![ダッシュボード](/assets/06-dashboard.png) + +ダッシュボードは、フィードバックデータを可視化し、重要な洞察を一目で理解できる機能です。 + +### 主な機能 + +- **リアルタイム統計**:フィードバック数、イシュー数、解決率などの主要指標をリアルタイム表示 +- **トレンド分析**:時間経過によるフィードバックとイシューのトレンドをグラフで表示 +- **イシュー分布**:イシュー別のフィードバック分布を可視化 + +### 提供されるチャートとウィジェット + +1. **フィードバックサマリーカード**:総フィードバック数、新規フィードバック、処理済みフィードバックなどの主要指標 +2. **時系列グラフ**:日次/週次/月次のフィードバックトレンド +3. **イシューステータスドーナツチャート**:イシューステータス別の分布 + +### 使用方法 + +1. ダッシュボードページにアクセス +2. 期間とフィルター設定を通じてデータ範囲を調整 +3. 主要指標とトレンドを分析 +4. 洞察に基づいた意思決定とアクションアイテムを導出 + +## 追加機能 + +上記の主要機能に加えて、ABC User Feedbackは以下の追加機能を提供します: + +### API連携 + +- RESTful APIを通じた外部システムとの統合 +- プログラムによるフィードバック収集と管理 + +### Webhook + +- 主要イベント発生時に外部システムに通知 +- 自動化されたワークフローの構築をサポート + +### 画像ストレージ連携 + +- S3互換ストレージを通じたユーザー提出画像の管理 +- フィードバックへのスクリーンショットと画像の添付 + +### データエクスポート + +- CSV、Excel形式でのフィードバックデータのエクスポート + +### 多言語サポート + +- 様々な言語でのインターフェース提供 +- 国際チーム向けの多言語フィードバック管理 + +--- + +このドキュメントは、ABC User Feedbackの主な機能の概要を提供します。各機能の詳細な使用方法については、[ユーザーガイド](/user-guide/getting-started)セクションを参照してください。 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/_category_.json b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/_category_.json new file mode 100644 index 000000000..e64d39fa0 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/00-introduction/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 1, + "label": "紹介" +} + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/01-getting-started.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/01-getting-started.md new file mode 100644 index 000000000..ffa747d2e --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/01-getting-started.md @@ -0,0 +1,263 @@ +--- +title: はじめに +description: ABC User Feedbackインストール後、初期設定から最初のフィードバック収集まで、システムを開始する方法を説明します。 +sidebar_position: 1 +--- + +# はじめに + +ABC User Feedbackを初めてインストールした後、システムを使用するには初期設定が必要です。このドキュメントでは、テナント作成から最初のフィードバック収集までの全プロセスを段階的に説明します。 + +--- + +## 初期設定の概要 + +ABC User Feedbackを開始するには、次の順序で設定を進めます: + +1. **テナントと管理者アカウントの作成** +2. **初回ログインとプロフィール設定** +3. **プロジェクトの作成** +4. **チャネルの作成とフィールド設定** +5. **APIキーの発行** +6. **最初のフィードバック収集のテスト** + +--- + +## システムへのアクセス + +ABC User Feedbackのインストールが必要な場合は、まず[Docker Hubイメージを使用したインストール](/ja/developer-guide/installation/docker-hub-images)を進めてください。 + +インストールを完了したら、Webブラウザを通じてABC User Feedbackにアクセスします: + +``` +http://localhost:3000 +``` + +> ポートやドメインを変更した場合は、設定に応じたアドレスを入力してください。 + +--- + +## テナントと管理者アカウントの作成 + +![member-register.png](/img/tenant.png) + +初めてアクセスすると、**テナント作成と管理者アカウント登録**画面が表示されます。 + +### Step 1: テナント情報の入力 + +テナント名を設定します。 + +テナント名を入力した後、**Next**ボタンをクリックします。 + +> このテナント名はログインUIに表示されます。 + +### Step 2: 管理者アカウントの作成 + +システムの最初の管理者アカウントを作成します。 + +1. 管理者アカウントのメールアドレスを入力し、**Request Code**ボタンをクリックします。 +2. メールボックスで認証コードを確認して入力します +3. **Verify**ボタンをクリックします +4. 認証が完了したら、パスワードを設定します。 + +:::info パスワード要件 + +- **8文字以上** +- **英字を含む**(A–Z、a–z) +- **特殊文字を含む**(例:`@`、`#`、`!`) +- **連続文字禁止**(例:`aa`、`11`) + +> **例**: ✅ `MyCompany2024!`, ❌ `12345678`、`password` + +::: + +テナントと管理者アカウントの作成が完了すると、確認画面が表示されます。 + +**次のステップ**: **確認**ボタンをクリックしてログイン画面に移動します。 + +--- + +## ログイン + +作成した管理者アカウントで初回ログインを行います。 + +1. **Email**: 先ほど登録した管理者メールアドレスを入力します +2. **Password**: 設定したパスワードを入力します +3. **Sign In**ボタンをクリックします + +--- + +## 最初のプロジェクトの作成 + +ログインすると、プロジェクト作成ウィザードが自動的に開始されます。 + +### システム構造の理解 + +ABC User Feedbackは、次の階層構造を持っています: + +``` +テナント(組織) + └── プロジェクト(製品/サービス単位) + └── チャネル(フィードバック収集パス) +``` + +### Step 1: プロジェクト基本情報 + +| 項目 | 説明 | 例 | +| --------------- | -------------------------------------- | ------------------------------- | +| **Name** | プロジェクト名 | `モバイルアプリ`、`Webサービス` | +| **Description** | プロジェクトの説明(オプション) | `顧客フィードバック収集と分析` | +| **Time Zone** | 時間基準(ダッシュボードと統計に影響) | `Asia/Tokyo` | + +**完了後**: 情報を入力した後、**Next**ボタンをクリックします。 + +### Step 2: チームメンバーの招待(オプション) + +このステップでは、プロジェクトにチームメンバーを招待できます。今スキップしても、後でいつでも追加できます。 + +### Step 3: APIキーの生成(オプション) + +外部システムとの統合用のAPIキーを事前に生成できます。 + +### プロジェクト作成完了 + +すべての情報を入力すると、プロジェクト作成が完了します。 + +**次のステップを選択**: + +- **Create Channel**: すぐにチャネルを作成してフィードバック収集を開始 +- **Skip for Now**: 後でチャネルを作成 + +--- + +## 最初のチャネルの作成 + +プロジェクト作成後、実際にフィードバックを収集するには**チャネル**を作成する必要があります。 + +### チャネルの概念の理解 + +チャネルは**フィードバック収集パス**を意味します: + +- ウェブサイトのお問い合わせフォーム +- モバイルアプリ内のフィードバック +- カスタマーサービスVoC +- アンケート回答 + +### Step 1: チャネル基本情報 + +| 項目 | 説明 | 例 | +| ---------------------------------- | ----------------------------------------------------- | ------------------------------------- | +| **Name** | チャネル名 | `Webフィードバック`、`アプリレビュー` | +| **Description** | チャネルの説明(オプション) | `ウェブサイトユーザーの意見` | +| **Maximum Feedback Search Period** | フィードバック検索可能期間(30/90/180/365日、すべて) | `90日` | + +**完了後**: 情報を入力した後、**Next**ボタンをクリックします。 + +### Step 2: フィールド設定 + +チャネルで収集するデータ構造を定義します。 + +#### デフォルトフィールド + +システムで自動的に作成されるフィールド: + +| フィールド名 | 形式 | プロパティ | 説明 | +| ------------ | ----------- | ---------- | -------------------------- | +| `id` | number | Read Only | フィードバック固有ID | +| `createdAt` | date | Read Only | 作成時間 | +| `updatedAt` | date | Read Only | 更新時間 | +| `issues` | multiSelect | Editable | リンクされたイシューリスト | + +#### カスタムフィールドの追加 + +実際のフィードバック収集のためにカスタムフィールドを追加します: + +1. **Add Field**ボタンをクリック +2. フィールド情報を入力: + +| 項目 | 説明 | 例 | +| ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- | +| **Key** | 一意の識別子(大文字/小文字、数字、`_`) | `message`、`rating` | +| **Display Name** | UIに表示される名前 | `フィードバック内容` | +| **Format** | データ形式 | `text`、`keyword`、`number`、`date`、`select`、`multiSelect`、`images`、`aiField` | +| **Property** | `Editable`(UIで変更可能)/ `Read Only`(変更不可) | `Editable` | +| **Status** | `Active` / `Inactive` | `Active` | + +#### 推奨デフォルトフィールド構成 + +最初のチャネルには、次のフィールドを追加することをお勧めします: + +| Key | Display Name | Format | 説明 | +| ----------- | ---------------------- | ------- | ---------------------- | +| `message` | フィードバック内容 | text | ユーザーフィードバック | +| `userEmail` | ユーザーメールアドレス | keyword | 連絡先(オプション) | +| `rating` | 満足度 | number | 1-5点評価 | + +### フィールドプレビュー + +フィールド設定を完了した後、**Preview**ボタンでフィードバック入力画面をプレビューできます。 + +**完了後**: **Complete**ボタンでチャネル作成を完了します。 + +### チャネル作成完了 + +**次のステップ**: **Start**ボタンをクリックしてフィードバック収集を開始します。 + +--- + +## 最初のフィードバック収集のテスト + +チャネル作成が完了すると、実際にフィードバックを収集できます。 + +### APIによるフィードバック登録 + +作成したAPIキーを使用して最初のフィードバックを登録してみましょう。 + +#### APIリクエスト例 + +```bash +curl -X POST http://localhost:4000/api/projects/1/channels/1/feedbacks \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: YOUR_API_KEY" \ + -d '{ + "message": "アプリの実行速度が遅いです", + "userEmail": "user@example.com", + "rating": 3 + }' +``` + +> `YOUR_API_KEY`は、先ほど作成した実際のAPIキーに置き換えてください。 + +#### 成功レスポンスの確認 + +APIリクエストが成功すると、次のようなレスポンスが返されます: + +```json +{ + "id": 1 +} +``` + +### フィードバックの確認 + +登録したフィードバックをWebインターフェースで確認してみましょう。 + +1. 上部メニューで**Feedback**タブをクリック +2. フィードバックリストで登録されたフィードバックを確認 +3. フィードバックをクリックして詳細情報を確認 + +### 最初のイシューの作成 + +フィードバックに基づいてイシューを作成してみましょう。 + +1. フィードバック詳細画面で、**Issue**セクションの**`+`ボタン**をクリック +2. イシュー名を入力し、**Enter**キーまたは**Create**ボタンをクリック +3. 作成されたイシューを確認 + +## 次のステップガイド + +基本的な設定と最初のフィードバック収集が完了しました! + +## 関連ドキュメント + +- [API統合](/ja/developer-guide/api-integration) - 詳細なAPI使用ガイド diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/02-project-management.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/02-project-management.md new file mode 100644 index 000000000..595971f45 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/02-project-management.md @@ -0,0 +1,347 @@ +--- +title: プロジェクト +description: ABC User Feedbackでプロジェクトを作成、設定、管理し、チームメンバーの役割と権限を設定する方法を説明します。 +sidebar_position: 2 +--- + +# プロジェクト + +ABC User Feedbackでは、**プロジェクト**はフィードバックを収集・分析する最も基本的な単位です。プロジェクトの作成からチーム管理、権限設定の機能を説明します。 + +--- + +## プロジェクトの概要 + +プロジェクトは次の階層構造を持ちます: + +``` +テナント + └── プロジェクト(複数可能) + ├── チャネル(複数可能) + ├── メンバーと役割 + ├── イシュートラッカー統合 + ├── ウェブフック統合 + ├── AI機能 + └── APIキー +``` + +各プロジェクトは複数のチャネルを含む管理単位で、チームメンバーと役割、イシュートラッカー統合、外部システム統合などを独立して設定・運用できます。 + +--- + +## プロジェクトの作成 + +### アクセス権限 + +プロジェクトの作成は**Superユーザー**のみ可能です。一般ユーザーは既存のプロジェクトにメンバーとして招待されて参加できます。 + +> Superユーザー権限が必要な場合は、システム管理者にお問い合わせください。 + +### アクセス方法 + +新しいプロジェクトを作成する方法は2つあります: + +1. **初回ログイン時**: プロジェクト作成ウィザードが自動的に開始されます +2. **追加プロジェクト**: 左サイドバー上部の**Create Project**ボタンをクリックします + +### Step 1: プロジェクト基本情報 + +![create-project-1](/img/project/1.png) + +プロジェクト作成時に次の情報を入力します: + +| 項目 | 説明 | 例 | +| --------------- | ---------------------------------------------------- | -------------------------------------------------------- | +| **Name** | プロジェクト名(必須) | `モバイルアプリ`、`カスタマーサービス`、`ベータサービス` | +| **Description** | 簡単な説明(オプション) | `iOS/Androidアプリユーザーフィードバック収集` | +| **Time Zone** | フィードバックとレポートの時間基準として使用(必須) | `Asia/Seoul` | + +> タイムゾーンは**ダッシュボード統計**に影響します。 + +**完了後**: すべての情報を入力した後、**Next**ボタンをクリックします。 + +### Step 2: チームメンバーの追加(オプション) + +![create-project-2](/img/project/2.png) + +このステップは**スキップ可能**です。後でプロジェクト設定からいつでも追加できます。 + +#### メンバーの追加 + +1. 右上の**Register Member**ボタンをクリックします +2. 次の項目を入力します: + - **Email**: システムに登録されたユーザーを選択 + - **Role**: Admin、Editor、Viewerから選択 + +> カスタム役割を使用したい場合は、**Role Management**ボタンをクリックして追加設定が可能です。 + +**完了後**: メンバーリストを確認して**Next**をクリックします。 + +### Step 3: APIキーの生成(オプション) + +![create-project-3](/img/project/3.png) + +APIキーは外部システムからフィードバックを収集する際に使用されます。設定メニューから後で生成できるため、今はスキップしてもかまいません。 + +#### キー生成方法 + +1. 右上の**Create API Key**ボタンをクリックします +2. キーが自動生成されてリストに表示されます +3. 生成されたキーをコピーして安全な場所に保存します + +### プロジェクト作成完了 + +![create-project-4](/img/project/4.png) + +すべてのステップを完了すると、**要約画面**が表示されます: + +- プロジェクト情報: 名前、説明、タイムゾーン +- メンバーリスト +- 生成されたAPIキー +- 役割設定状態 + +#### 次のステップ + +- すぐにチャネルを作成してフィードバック収集を開始するには、**Create Channel**ボタンをクリックします +- または**Later**ボタンで後で作成できます + +--- + +## プロジェクト設定の管理 + +![project-setting.png](/img/project/project-setting.png) + +### アクセス方法 + +プロジェクト設定を変更するには: + +1. 上部メニューで**Settings**をクリック +2. 左メニューから**Project Setting**を選択 + +### 基本情報の編集 + +次の項目をいつでも変更できます: + +| 項目 | 説明 | 注意事項 | +| --------------- | ---------------------------------- | ---------------------------------- | +| **Name** | プロジェクト名 | チームメンバーに表示される名前 | +| **Description** | 説明(オプション) | プロジェクトの目的 | +| **Time Zone** | 統計と時間関連データの基準時刻設定 | 変更時、既存データには影響しません | + +**保存方法**: 変更後、右上の**Save**ボタンをクリックします。 + +### タイムゾーン変更時の注意事項 + +- 既存のフィードバック/イシューの時間情報には影響しません +- 変更後、ダッシュボード統計でデータの不一致が発生する可能性があります。 + +### プロジェクトの削除 + +#### 削除手順 + +プロジェクトを完全に削除するには: + +1. Project Setting画面下部の**Delete Project**ボタンをクリックします +2. 確認ポップアップでプロジェクト名を正確に入力します +3. **Delete**ボタンで最終確定します + +#### 削除時の注意事項 + +- そのプロジェクト内の**すべてのフィードバック、イシュー、設定が永続的に削除**されます +- **元に戻せないため**、事前のバックアップまたはエクスポートを推奨します +- 削除時、接続されたチャネルとAPIキーも一緒に削除されます + +--- + +## メンバー管理 + +![member-setting.png](/img/project/member-setting.png) + +### メンバーリストの確認 + +現在プロジェクトに参加しているメンバーを確認するには: + +1. 上部メニューで**Settings**をクリック +2. 左メニューから**Member Management**を選択 + +メンバーリストには次の情報が表示されます: + +| 項目 | 説明 | +| ---------- | ------------------------------ | +| Email | アカウントメールアドレス | +| Name | ユーザー名(プロフィール基準) | +| Department | 所属部門 | +| Role | プロジェクト内の役割 | +| Joined | プロジェクト参加日 | + +### 新しいメンバーの招待 + +![member-register.png](/img/project/member-register.png) + +#### 招待手順 + +1. **Register Member**ボタンをクリックします +2. 招待情報を入力します: + +| 項目 | 説明 | +| --------- | ------------------------------------------- | +| **Email** | 招待するユーザーのメールアドレス | +| **Role** | 割り当てる役割(Admin、Editor、Viewerなど) | + +3. **Invite**ボタンをクリックして招待を完了します + +### メンバー情報の編集 + +既存のメンバー情報を変更するには: + +1. メンバーリストで編集したいメンバーの行をクリックします +2. ポップアップでRoleを変更できます: +3. **Save**ボタンで変更を保存します + +### メンバーの削除 + +メンバーをプロジェクトから削除するには: + +1. メンバー編集ポップアップ下部の**削除**ボタンをクリックします +2. 確認メッセージで**確認**をクリックします + +> メンバーを削除しても、そのユーザーが作成したフィードバック/イシュー記録はそのまま維持され、プロジェクトアクセス権限のみが削除されます。 + +--- + +## 役割と権限管理 + +![role-setting.png](/img/project/role-setting.png) + +### デフォルト役割 + +システムでは次のデフォルト役割を提供します: + +| 役割 | 権限要約 | +| ---------- | --------------------------------------------------------- | +| **Admin** | すべての機能にアクセス可能。プロジェクト削除を含む | +| **Editor** | フィードバック/イシューの作成、編集、削除可能。設定は不可 | +| **Viewer** | 閲覧のみ可能。編集、削除、設定アクセス不可 | + +### カスタム役割の作成 + +![role-create.png](/img/project/role-create.png) + +より細分化された権限が必要な場合は、カスタム役割を作成できます: + +1. Member Management画面で**Role Management**リンクをクリックします +2. **Create Role**ボタンをクリックします +3. 役割名と権限を入力します: + +### 権限設定 + +各役割について、次の機能別権限を設定できます: + +#### フィードバック権限 + +| 権限項目 | 説明 | +| ----------------------------------- | ------------------------------------- | +| **Download Feedback** | フィードバックデータのダウンロード | +| **Edit Feedback** | フィードバックの編集 | +| **Delete Feedback** | フィードバックの削除 | +| **Attach/Detach Issue in Feedback** | フィードバックとイシューのリンク/解除 | + +#### イシュー権限 + +| 権限項目 | 説明 | +| ---------------- | -------------- | +| **Create Issue** | イシューの作成 | +| **Edit Issue** | イシューの編集 | +| **Delete Issue** | イシューの削除 | + +#### プロジェクト管理 + +| 権限項目 | 説明 | +| --------------------- | ---------------------- | +| **Edit Project Info** | プロジェクト情報の編集 | +| **Delete Project** | プロジェクトの削除 | + +#### メンバー管理 + +| 権限項目 | 説明 | +| ------------------------- | -------------------------- | +| **Read Project Member** | プロジェクトメンバーの閲覧 | +| **Create Project Member** | プロジェクトメンバーの招待 | +| **Edit Project Member** | プロジェクトメンバーの編集 | +| **Delete Project Member** | プロジェクトメンバーの削除 | + +#### 役割管理 + +| 権限項目 | 説明 | +| ----------------------- | ---------------------- | +| **Read Project Role** | プロジェクト役割の閲覧 | +| **Create Project Role** | プロジェクト役割の作成 | +| **Edit Project Role** | プロジェクト役割の編集 | +| **Delete Project Role** | プロジェクト役割の削除 | + +#### APIキー管理 + +| 権限項目 | 説明 | +| ------------------ | ------------- | +| **Read API Key** | APIキーの閲覧 | +| **Create API Key** | APIキーの作成 | +| **Edit API Key** | APIキーの編集 | +| **Delete API Key** | APIキーの削除 | + +#### イシュートラッカー + +| 権限項目 | 説明 | +| ---------------------- | ------------------------ | +| **Read Issue Tracker** | イシュートラッカーの閲覧 | +| **Edit Issue Tracker** | イシュートラッカーの設定 | + +#### ウェブフック管理 + +| 権限項目 | 説明 | +| ------------------ | ------------------ | +| **Read Webhook** | ウェブフックの閲覧 | +| **Create Webhook** | ウェブフックの作成 | +| **Edit Webhook** | ウェブフックの編集 | +| **Delete Webhook** | ウェブフックの削除 | + +#### AIとチャネル設定 + +| 権限項目 | 説明 | +| ---------------------- | ------------ | +| **Read Generative AI** | AI設定の閲覧 | +| **Edit Generative AI** | AI設定の編集 | + +#### チャネル関連設定 + +| 権限項目 | 説明 | +| ---------------------- | -------------------- | +| **Edit Channel Info** | チャネル情報の編集 | +| **Delete Channel** | チャネルの削除 | +| **Read Field** | フィールドの閲覧 | +| **Edit Field** | フィールドの編集 | +| **Read Image Setting** | 画像設定の閲覧 | +| **Edit Image Setting** | 画像設定の編集 | +| **Create Channel** | 新しいチャネルの作成 | + +### 権限設定のヒント + +#### セキュリティのベストプラクティス + +- **最小権限の原則**: 業務に必要な最小限の権限のみを付与 +- **定期的なレビュー**: チーム変更や退職者発生時に権限を確認 +- **Admin役割の制限**: 管理者は可能な限り少ない数で維持 + +### 役割の編集と削除 + +- **編集**: 役割リストで希望する項目をクリックして名前と権限を変更できます +- **削除**: 使用されていない役割は**Delete**ボタンで削除可能です + +> **注意**: Admin役割は常に1つ以上存在する必要があり、削除できません。 + +--- + +## 関連ドキュメント + +- [チャネル管理](./03-channel-management.md) - チャネルの作成とフィールド設定 +- [フィードバック管理](./04-feedback-management.md) - フィードバックの収集と分析 +- [API統合](/ja/developer-guide/api-integration) - APIキーの使用方法 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/03-channel-management.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/03-channel-management.md new file mode 100644 index 000000000..daeaae537 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/03-channel-management.md @@ -0,0 +1,235 @@ +--- +title: チャネル +description: ABC User Feedbackでフィードバック収集チャネルを作成、設定、管理し、カスタムフィールドと画像設定を扱う方法を説明します。 +sidebar_position: 3 +--- + +# チャネル + +**チャネル(Channel)**は、フィードバック収集パスまたは目的に応じて区別される単位です。各チャネルは独立したフィールド構造、画像設定、AI機能を持ち、さまざまなフィードバック収集シナリオに合わせて設定できます。 + +--- + +## チャネルの概要 + +### チャネルの役割 + +チャネルは次の役割を果たします: + +- **フィードバック収集パスの区別**: Web、アプリ、カスタマーサービス、アンケートなど +- **データ構造の定義**: チャネル別の固有フィールド設定 +- **収集ポリシーの管理**: 画像許可、検索期間、セキュリティ設定など +- **分析単位の提供**: チャネル別の独立した統計と分析 + +--- + +## チャネルの作成 + +### アクセス方法 + +新しいチャネルを作成する方法: + +1. **プロジェクト作成直後**: プロジェクト完了画面で**Create Channel**ボタンをクリック +2. **追加チャネル作成**: **Settings > Channel List**で**Create Channel**ボタンをクリック + +### Step 1: チャネル基本情報 + +![channel-create-1](/img/channel/1.png) + +| 項目 | 説明 | 例 | +| ---------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- | +| **Name** | チャネル名(必須) | `Webフィードバック`、`アプリレビュー`、`カスタマーサービス` | +| **Description** | チャネルの簡単な説明(オプション) | `ウェブサイトユーザーの意見収集` | +| **Maximum Feedback Search Period** | フィードバック検索可能な最大期間(30/90/180/365日、すべて) | `90日` | + +#### 最大フィードバック検索期間設定時の注意事項 + +- **影響範囲**: フィードバックダウンロード機能に直接影響します +- **ダウンロード動作**: 設定された検索期間内のすべてのフィードバックがダウンロード対象になります +- **パフォーマンステスト**: 1日のフィードバック数が多い場合、さまざまな期間でテストして最適値を探すことをお勧めします +- **段階的調整**: 最初は短い期間から始め、必要に応じて段階的に延長するのが安全です + +**完了後**: 情報を入力した後、**Next**ボタンをクリックします。 + +### Step 2: フィールド設定 + +![channel-create-2](/img/channel/2.png) + +チャネルで収集するデータ構造を定義します。これはAPIリクエスト構造とフィードバックテーブル構成に直接影響します。 + +#### デフォルトシステムフィールド + +すべてのチャネルに自動的に含まれるフィールド: + +| Key | Format | プロパティ | 説明 | +| ----------- | ----------- | ---------- | -------------------------- | +| `id` | number | Read Only | フィードバック固有ID | +| `createdAt` | date | Read Only | フィードバック作成時刻 | +| `updatedAt` | date | Read Only | フィードバック更新時刻 | +| `issues` | multiSelect | Editable | リンクされたイシューリスト | + +> これらのフィールドは削除したり、主要なプロパティを変更したりできません。 + +#### カスタムフィールドの追加 + +実際のビジネス要件に合わせたフィールドを追加します。 + +1. **Add Field**ボタンをクリックします +2. フィールド情報を入力します + +| 項目 | 説明 | 例 | +| ---------------- | ----------------------------------------------- | ---------------------------------------------- | +| **Key** | 一意の識別子(大文字/小文字、数字、`_`) | `message`、`rating` | +| **Display Name** | UIに表示される名前 | `フィードバック内容`、`ユーザーメールアドレス` | +| **Format** | データ形式(下の表を参照) | `text`、`keyword`、`number` | +| **Property** | `Editable`(入力可能)/ `Read Only`(閲覧のみ) | `Editable` | +| **Status** | `Active` / `Inactive` | `Active` | +| **Description** | チームメンバーが理解しやすい説明(オプション) | `ユーザーが入力したフィードバック内容` | + +### フィールド形式の種類 + +| Format | 説明 | 使用例 | API例 | +| ------------- | -------------------- | -------------------------------- | -------------------------------- | +| `text` | 自由テキスト入力 | フィードバック内容、詳細説明 | `"アプリが頻繁にフリーズします"` | +| `keyword` | 短いキーワード/タグ | バージョン情報、ページ名 | `"v1.2.3"` | +| `number` | 数字 | 評価、年齢、使用時間 | `5` | +| `date` | 日付 | 発生日、有効期限 | `"2024-03-01T00:00:00Z"` | +| `select` | 単一選択 | カテゴリ、優先度 | `"機能リクエスト"` | +| `multiSelect` | 複数選択 | タグ、関連機能 | `["バグ", "UI"]` | +| `images` | 画像URL配列 | スクリーンショット、添付ファイル | `["https://..."]` | +| `aiField` | AI分析結果フィールド | 感情分析、要約、キーワード抽出 | `"ポジティブ"` | + +> **images形式について**: 詳細な画像設定方法については、[画像設定](/ja/user-guide/settings/image-setting)ドキュメントを参照してください。 +> +> **aiField形式について**: AIフィールド設定とテンプレート構成方法については、[AI設定](/ja/user-guide/settings/ai-setting)ドキュメントを参照してください。 + +### フィールド構成例 + +#### Webフィードバックチャネル + +| Key | Display Name | Format | 用途 | +| ------------- | ------------------ | ------- | ---------------------------- | +| `message` | フィードバック内容 | text | ユーザーの意見 | +| `userEmail` | メールアドレス | keyword | 連絡先(オプション) | +| `pageUrl` | ページURL | keyword | フィードバック発生位置 | +| `category` | カテゴリ | select | バグ/機能リクエスト/改善事項 | +| `priority` | 優先度 | select | 高/中/低 | +| `screenshots` | スクリーンショット | images | 問題状況のキャプチャ | + +#### モバイルアプリレビューチャネル + +| Key | Display Name | Format | 用途 | +| ------------ | ---------------- | ------- | ---------------- | +| `message` | レビュー内容 | text | ユーザーレビュー | +| `rating` | 評価 | number | 1-5点評価 | +| `appVersion` | アプリバージョン | keyword | バグ追跡用 | +| `deviceType` | デバイスタイプ | select | iOS/Android | +| `crashLogs` | クラッシュログ | text | 技術的エラー情報 | + +### フィールドプレビュー + +フィールド設定を完了した後、**Preview**ボタンで実際のフィードバック入力画面をプレビューできます。 + +このプレビューは、APIリクエストに必要なフィールド構造と同じです。 + +**完了後**: **Next**ボタンで次のステップに進みます。 + +### Step 3: チャネル作成完了 + +![create-channel-3](/img/channel/3.png) + +すべてのステップを完了すると、**要約画面**が表示されます: + +- チャネル情報:名前、説明、タイムゾーン +- フィールド情報 + +--- + +## フィールド管理 + +### フィールドの編集 + +既存のフィールドを編集するには、フィールドリストで編集したいフィールドの行をクリックして情報を変更します。 + +> **注意**: `Key`と`Format`は作成後に変更できません。データの一貫性のために制限されています。 + +### フィールドの削除 + +フィードバックデータの整合性と一貫性を保証するため、**フィールド削除機能は提供されていません**。 + +#### 削除の代わりに推奨される方法 + +1. **Inactive状態に変更**: フィールドを無効化して新しいフィードバック収集から除外 +2. **データの保持**: 既に収集されたフィードバックデータはそのまま維持 +3. **フィルタリングの活用**: フィールドリストでActiveフィールドのみを表示して管理効率を確保 + +#### 完全削除が必要な場合 + +フィールドを完全に削除する必要がある状況では: + +- チャネル全体を削除して新しく作成する方法を検討 +- データをエクスポート後、新しい構造に移行 +- 開発チームと相談してデータベースレベルで処理 + +### フィールドステータス管理 + +#### Active / Inactiveの切り替え + +- **Active**: フィードバック収集時に使用されるフィールド +- **Inactive**: 一時的に無効化されたフィールド(データは保持) + +#### フィルタリングオプション + +上部のコントロールで次の条件でフィールドをフィルタリングできます: + +- **Status**: `Active` / `Inactive` +- **Property**: `Editable` / `Read Only` + +--- + +## チャネル情報管理 + +![channel-setting](/img/channel/channel-setting.png) + +### チャネル基本情報の編集 + +作成されたチャネルの基本情報を変更できます。 + +#### アクセス方法 + +1. **Settings > Channel List > [チャネル選択]** +2. **Channel Information**タブをクリック + +#### 編集可能な項目 + +| 項目 | 編集可能 | 注意事項 | +| ---------------------------------- | -------- | ---------------------------------------- | +| **Channel ID** | ❌ 不可 | システム内部識別子 | +| **Channel Name** | ✅ 可能 | チームメンバーに表示される名前 | +| **Description** | ✅ 可能 | チャネルの目的 | +| **Maximum Feedback Search Period** | ✅ 可能 | パフォーマンスに影響する可能性があります | + +### チャネルの削除 + +使用しなくなったチャネルを削除できます。 + +#### 削除手順 + +1. Channel Information画面下部の**Delete Channel**ボタンをクリック +2. 確認ポップアップでチャネル名を正確に入力 +3. **Delete**ボタンで最終確定 + +#### 削除時の注意事項 + +- そのチャネルの**すべてのフィードバックデータが永続的に削除**されます +- **元に戻せないため**、事前のバックアップまたはエクスポートを推奨します +- 関連するAPIキー設定も確認が必要です + +--- + +## 関連ドキュメント + +- [プロジェクト管理](./02-project-management.md) - プロジェクト設定と権限管理 +- [フィードバック管理](./04-feedback-management.md) - 収集されたフィードバックの分析と活用 +- [API統合](/ja/developer-guide/api-integration) - 外部システムとの統合方法 +- [AI統合](/ja/user-guide/settings/ai-setting) - AI機能設定 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/04-feedback-management.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/04-feedback-management.md new file mode 100644 index 000000000..27161b1cd --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/04-feedback-management.md @@ -0,0 +1,336 @@ +--- +title: フィードバック +description: ABC User Feedbackでフィードバックを作成、閲覧、分析、管理する方法を説明します。 +sidebar_position: 4 +--- + +# フィードバック + +フィードバックはABC User Feedbackのコアデータです。このドキュメントでは、フィードバックの作成から分析、管理まで、フィードバックに関連するすべての機能を説明します。 + +![feedback](/img/feedback/0.png) + +--- + +## フィードバックの作成 + +フィードバックは主に外部システム(ウェブサイト、モバイルアプリ、API統合)によって作成されますが、管理者が直接作成することもできます。 + +### APIによるフィードバック作成 + +最も一般的なフィードバック作成方法です。 + +#### 基本APIリクエスト構造 + +```bash +curl -X POST http://your-domain.com/api/v1/projects/{projectId}/channels/{channelId}/feedbacks \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: YOUR_API_KEY" \ + -d '{ + "message": "ユーザーフィードバック内容", + "userEmail": "user@example.com", + "category": "バグレポート" + }' +``` + +#### チャネルフィールドに応じたリクエスト例 + +各チャネルのフィールド設定に応じてリクエスト構造が異なります: + +**Webフィードバックチャネル例**: + +```json +{ + "message": "ログインボタンが動作しません", + "userEmail": "user@company.com", + "pageUrl": "https://example.com/login", + "category": "バグ", + "priority": "高", + "browserInfo": "Chrome 119.0.0" +} +``` + +**モバイルアプリチャネル例**: + +```json +{ + "message": "アプリが頻繁にフリーズします", + "rating": 2, + "appVersion": "v2.1.3", + "deviceType": "iOS", + "crashLogs": "Exception in thread main..." +} +``` + +#### 画像を含むフィードバック + +画像URL方式を使用する場合: + +```json +{ + "message": "画面が壊れて表示されます", + "userEmail": "user@example.com", + "images": [ + "https://cdn.example.com/screenshot1.png", + "https://cdn.example.com/screenshot2.png" + ] +} +``` + +### フィードバック作成の確認 + +作成されたフィードバックはすぐにフィードバックリストに表示されます。 + +--- + +## フィードバックリストへのアクセス + +作成されたフィードバックを確認・管理するためにフィードバックリストにアクセスします。 + +### アクセス方法 + +1. 左サイドバーから目的の**プロジェクト**を選択します +2. 下部のチャネルリストから目的の**チャネル**をクリックします +3. 上部メニューで**Feedback**タブを選択します + +### フィードバックテーブル構成 + +フィードバックリストはテーブル形式で表示され、次の基本構造を持ちます: + +| 列タイプ | 説明 | 例 | +| ---------------- | ---------------------------------- | ---------------------------- | +| **デフォルト列** | すべてのチャネルに共通表示 | ID、Created、Updated、Issue | +| **カスタム列** | チャネルフィールド設定に応じて表示 | Message、UserEmail、Category | + +--- + +## フィードバックフィルタリング/ソート/ビューオプション + +大量のフィードバックデータから目的の情報を素早く見つけるためのさまざまなツールを提供します。 + +![feedback-option](/img/feedback/1.png) + +### 日付フィルタリング + +上部の**Date**ボタンで閲覧期間を設定できます。 + +#### 提供される期間オプション + +| オプション | 説明 | 使用例 | +| ------------ | ---------------------------- | ------------------------ | +| **今日** | 当日登録されたフィードバック | リアルタイムモニタリング | +| **昨日** | 前日のフィードバック | 日次レビュー | +| **過去7日** | 最近1週間のデータ | 週次分析 | +| **過去30日** | 最近1ヶ月のデータ | 月次トレンド把握 | +| **カスタム** | 開始日-終了日を直接設定 | 特定期間分析 | + +### 高度なフィルタ + +**Filter**ボタンをクリックすると、さまざまな条件でフィードバックをフィルタリングできます。 + +![feedback-filter](/img/feedback/2.png) + +#### フィルタ構造 + +``` +Where: 最初の条件 +And: すべての条件を満たすフィードバック +Or: いずれかを満たすフィードバック +``` + +> **注意**: `And`と`Or`は同時に混在して使用できません。 + +#### フィールドタイプ別フィルタオプション + +| フィールドタイプ | 使用可能な演算子 | 例 | +| ---------------- | ------------------------------------ | ---------------------------- | +| **text** | Contains(部分一致) | message contains "バグ" | +| **keyword** | Is(完全一致) | category is "機能リクエスト" | +| **number** | Is(完全一致) | rating == 3 | +| **select** | Is(完全一致) | | +| **multiSelect** | Is(完全一致)、Contains(部分一致) | | +| **aiField** | Contains(部分一致) | | +| **date** | Is(完全一致)、Between(期間一致) | created between 日付範囲 | + +#### フィルタ使用例 + +**マルチセレクトカテゴリの高度な検索**: + +``` +Where: category contains "バグ" +And: priority is "高" +``` + +**複数のイシューがリンクされたフィードバックを検索**: + +``` +Where: issues contains "ログインイシュー" +Or: issues contains "UI改善" +``` + +**特定カテゴリの4評価フィードバックを検索**: + +``` +Where: category is "機能リクエスト" +And: rating is 4 +``` + +### ソート機能 + +テーブルヘッダーをクリックして、その列基準でソートできます。Created列とUpdated列でこの機能が提供されます。 + +### ビューオプション + +フィードバックリストの表示方法をユーザーのニーズに合わせて調整できます。 + +#### Expand機能 + +**Expand**ボタンをクリックすると、テーブルで各フィードバックの詳細内容をプレビューできます。 + +**活用方法**: + +- 詳細パネルを開かずに主要な内容を確認 +- 複数のフィードバックを素早く閲覧 +- 長いテキストフィールドの全体内容を確認 + +#### 列の表示/非表示 + +テーブル上部の**View**ボタンで表示する列を選択できます。 + +**機能**: + +- **必須列**: ID、Createdは常に表示(非表示不可) +- **オプション列**: カスタムフィールドを個別に表示/非表示設定 +- **画面最適化**: 必要な情報のみを表示して画面スペースを効率的に活用 + +**使用のヒント**: + +``` +モニタリング用: ID、Created、Messageのみ表示 +分析用: すべてのカスタムフィールドを表示 +レビュー用: Message、Category、Priorityを表示 +``` + +## フィードバックの確認/編集/削除 + +個別のフィードバックの詳細情報を確認し、必要に応じて編集または削除できます。 + +### フィードバック詳細表示 + +#### アクセス方法 + +フィードバックテーブルで**行をクリック**すると、右側に詳細表示パネルが開きます。 + +![feedback-detail](/img/feedback/3.png) + +### 詳細パネル構成 + +詳細パネルは次のように構成されます: + +#### 1. 基本情報エリア + +- **フィードバックID**: 一意の識別番号 +- **作成時刻**: 初回登録日時 +- **更新時刻**: 最後の変更日時 +- **イシュー**: タグ付けされたイシュー + +#### 2. カスタムフィールドエリア + +チャネルで設定したすべてのカスタムフィールドが表示されます。 + +### フィードバックの編集 + +#### 編集可能なフィールド + +詳細パネルで**Edit**ボタンをクリックして編集モードに切り替えることができます。 + +#### 編集可能な項目 + +| 項目 | 編集可能 | 注意事項 | +| ------------------------ | -------- | ---------------------------------------------------------------------- | +| **デフォルトフィールド** | ❌ 不可 | ID、作成日など | +| **カスタムフィールド** | ✅ 可能 | フィールド設定のPropertyによって異なる、StatusがInactiveの場合は不可能 | + +#### 編集完了 + +1. 必要な情報を編集します +2. **Save**ボタンをクリックします +3. 変更内容がすぐに反映され、"Updated"時刻が更新されます + +### イシューリンク管理 + +#### 新しいイシューの作成 + +1. イシュー列の**+ボタン**をクリックします +2. イシュー名を入力し、**Create**オプションをクリックします + +#### 既存イシューのリンク + +1. イシューセクションの**+ボタン**をクリックします +2. リンクするイシューの名前を入力します +3. ドロップダウンからリンクするイシューを選択します + +#### イシューリンクの解除 + +1. イシューセクションの**+ボタン**をクリックします +2. 解除するイシューを選択します + +### フィードバックの削除 + +#### 単一フィードバックの削除 + +1. 詳細パネル下部の**Delete Feedback**ボタンをクリックします +2. 確認ダイアログで削除を承認します + +#### 複数フィードバックの削除 + +1. フィードバックリストで**チェックボックス**を使用して複数のフィードバックを選択します +2. 上部に表示される**Delete Selected**ボタンをクリックします +3. 一括削除を確認します + +#### 削除時の注意事項 + +- **復元不可能**: 削除されたフィードバックは元に戻せません +- **イシューリンク解除**: リンクされたイシューはそのまま維持されますが、リンクが解除されます +- **統計への影響**: ダッシュボード統計から該当データが除外されます + +--- + +## フィードバックのダウンロード + +収集されたフィードバックデータを分析またはバックアップするために、さまざまな形式でエクスポートできます。 + +### ダウンロード機能へのアクセス + +#### すべてのフィードバックのダウンロード + +1. フィードバックリスト上部の**Export**ボタンをクリックします + +#### フィルタされたフィードバックのダウンロード + +1. 希望する条件でフィルタリングを適用します +2. **Export**ボタンをクリックして、現在のフィルタ条件に一致するデータのみをダウンロードします + +#### 選択されたフィードバックのダウンロード + +1. チェックボックスで特定のフィードバックを選択します +2. **Export Selected**ボタンをクリックします + +### ダウンロード形式の選択 + +Exportボタンをクリックすると、ダウンロード形式を選択できます。 + +#### サポートされる形式 + +| 形式 | 拡張子 | 利点 | 推奨使用例 | +| --------- | ------- | ------------------------ | ------------------------ | +| **CSV** | `.csv` | 軽量で互換性が高い | Excel、Google Sheets分析 | +| **Excel** | `.xlsx` | 書式保持、複数シート対応 | 詳細分析、レポート作成 | + +--- + +## 関連ドキュメント + +- [チャネル管理](./03-channel-management.md) - フィードバック収集のためのチャネルとフィールド設定 +- [イシュー管理](./05-issue-management.md) - フィードバックからイシューを作成・管理 +- [API統合](/ja/developer-guide/api-integration) - 外部システムからフィードバックを送信する方法 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/05-issue-management.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/05-issue-management.md new file mode 100644 index 000000000..53e71a103 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/05-issue-management.md @@ -0,0 +1,314 @@ +--- +title: イシュー +description: ABC User Feedbackでイシューを作成、管理し、カンバン/リストビューで効率的に追跡する方法を説明します。 +sidebar_position: 5 +--- + +# イシュー + +**イシュー(Issue)**は、フィードバックで発見された問題点や改善点を体系的に管理するためのコア機能です。イシューの作成からカテゴリ管理、さまざまなビューモードの活用まで、イシュー管理のすべての機能を説明します。 + +![issue](/img/issue/1.png) + +--- + +## イシューの概要 + +### イシューの役割 + +イシューは次の目的で使用されます: + +- **問題の追跡**: バグ、エラー、パフォーマンス問題などを体系的に管理 +- **機能リクエスト管理**: ユーザーリクエストを構造化して開発計画に反映 +- **改善点の導出**: フィードバック分析による改善ポイントの特定 + +### イシューステータス + +各イシューは次のステータスを持ちます: + +| ステータス | 説明 | 使用タイミング | +| --------------- | ---------- | ---------------------- | +| **New** | 新規登録 | イシュー初回作成 | +| **On Review** | レビュー中 | 担当者がレビュー開始 | +| **In Progress** | 処理中 | 実際の作業進行中 | +| **Resolved** | 解決完了 | 問題解決および完了 | +| **On Hold** | 一時保留 | 追加情報待ちまたは延期 | + +--- + +## イシューの作成/編集/削除 + +### イシュー作成方法 + +イシューは2つの方法で作成できます。 + +#### 1. フィードバックからイシュー作成(推奨) + +最も一般的な方法で、特定のフィードバックに基づいてイシューを作成します。 + +1. **Feedback**タブでフィードバックをクリックして詳細表示を開きます +2. 右側の詳細パネルの**Issue**セクションで**`+`ボタン**をクリックします +3. イシュー名を入力し、**Enter**キーを押すか**Create**オプションをクリックします + +#### 2. イシューリストから直接作成 + +1. 上部メニューで**Issue**タブをクリックします +2. 左上の**+ Create Issue**ボタンをクリックします +3. イシュー作成ダイアログで情報を入力します: + +| 項目 | 説明 | 必須 | 例 | +| --------------- | --------------------------------- | ---------- | ------------------------ | +| **Title** | イシュータイトル | 必須 | `ログインボタンの誤動作` | +| **Description** | 詳細説明 | オプション | `特定のブラウザで発生` | +| **Category** | イシュー分類 | オプション | `バグ` | +| **Status** | 初期ステータス(デフォルト: New) | オプション | `New` | + +### イシューの編集 + +作成されたイシューの情報を変更できます。 + +#### 編集方法 + +1. イシューリストで編集したいイシューをクリックします +2. 右側に開く**Issue Details**パネルで**Edit**ボタンをクリックします +3. 編集モードで次の項目を変更できます: + +#### 編集可能な項目 + +| 項目 | 編集可能 | 説明 | +| --------------- | -------- | -------------------------------------- | +| **Title** | ✅ 可能 | イシュータイトル | +| **Description** | ✅ 可能 | 詳細説明 | +| **Category** | ✅ 可能 | イシュー分類(ドロップダウンから選択) | +| **Status** | ✅ 可能 | 現在の進行状況 | +| **Ticket** | ✅ 可能 | 外部イシュートラッカーチケット番号 | +| **ID** | ❌ 不可 | システム自動生成 | +| **Created** | ❌ 不可 | 作成日時 | + +#### 保存とキャンセル + +- **Save**ボタン: 変更を保存して編集モードを終了します +- **Cancel**ボタン: 変更をキャンセルして元の状態に戻します + +### 外部イシュートラッカー統合 + +外部イシュートラッカー(Jiraなど)との統合が設定されている場合、イシューに外部チケットをリンクできます。 + +#### チケットリンク方法 + +1. イシュー詳細パネルの**Ticket**フィールドに外部チケット番号を入力します +2. 入力された番号は自動的に外部システムリンクに変換されます + +> **参考**: 外部イシュートラッカー統合は**Settings > Issue Tracker Management**で事前設定が必要です。 + +### イシューの削除 + +不要になったイシューを削除できます。 + +#### 削除方法 + +1. イシュー詳細パネルで**Delete**ボタンをクリックします + +2. 確認ダイアログで削除を承認します + +#### 削除時の注意事項 + +- **復元不可能**: 削除されたイシューは元に戻せません +- **フィードバックリンク解除**: リンクされたフィードバックのイシューリンクが削除されます +- **統計への影響**: ダッシュボードイシュー統計から該当データが除外されます + +--- + +## カンバンビュー + +カンバンビューは、イシューをステータス別の列に分けて視覚的に管理できるビュー方式です。 + +![issue-kanban](/img/issue/2.png) + +### カンバンビューへのアクセス + +1. 上部メニューで**Issue**タブをクリックします +2. 右上で**Kanban**ビューを選択します + +### カンバンボード構成 + +各ステータス別に列が構成され、イシューがカード形式で表示されます。 + +#### カンバン列構成 + +| 列 | 表示情報 | カード数表示 | +| --------------- | ---------------------- | ------------ | +| **New** | 新規登録されたイシュー | 上部に数字 | +| **On Review** | レビュー中のイシュー | 上部に数字 | +| **In Progress** | 進行中のイシュー | 上部に数字 | +| **Resolved** | 解決完了したイシュー | 上部に数字 | +| **On Hold** | 保留されたイシュー | 上部に数字 | + +#### イシューカード情報 + +各イシューカードには次の情報が表示されます: + +- **イシュータイトル**: クリックで詳細表示に移動 +- **フィードバック数**: リンクされたフィードバック数(📝アイコンとともに) +- **カテゴリ**: 設定されている場合は下部に表示 +- **外部チケット**: リンクされている場合はチケット番号を表示 + +### ドラッグアンドドロップによるステータス変更 + +カンバンビューのコア機能で、イシューカードをドラッグして別の列に移動させてステータスを変更できます。 + +#### 使用方法 + +1. イシューカードをマウスでクリックしてドラッグします +2. 希望するステータス列の上に移動させます +3. マウスを離すとステータスが自動的に変更されます + +### カンバンビューフィルタリング + +上部のフィルタ機能を使用して、特定条件のイシューのみを表示できます。 + +#### 使用可能なフィルタ + +1. **Date**フィルタ: 特定期間に作成されたイシューのみを表示 +2. **Filter**ボタン: 高度なフィルタ条件設定 + +#### フィルタ条件例 + +| フィルタタイプ | 条件例 | 使用例 | +| -------------- | ------------------------- | -------------------------------- | +| **Category** | Category = "バグ" | バグイシューのみ確認 | +| **Title** | Title contains "ログイン" | ログイン関連イシュー検索 | +| **Created** | Created >= 2024-03-01 | 特定日付以降に作成されたイシュー | +| **Status** | Status != "Resolved" | 未解決イシューのみ表示 | + +### カンバンビューソート + +各列内でイシューカードのソート順序を変更できます。 + +#### ソートオプション + +- **Created Date ↓**: 最新作成順 +- **Created Date ↑**: 古い順 +- **Feedback Count ↓**: リンクされたフィードバック数が多い順 + +--- + +## リストビュー + +リストビューは、イシューをカテゴリ別にグループ化してテーブル形式で表示するビュー方式です。 + +### リストビューへのアクセス + +1. 上部メニューで**Issue**タブをクリックします +2. 右上で**List**ビューを選択します + +### リストビュー構成 + +カテゴリ別にグループ化されたイシューが階層的に表示されます。 + +#### カテゴリグループ + +各カテゴリは折りたたみ/展開可能なグループとして表示されます: + +- **グループヘッダー**: カテゴリ名と含まれるイシュー数 +- **折りたたみ/展開矢印**: グループ内容の表示/非表示を切り替え +- **"No Category"**: カテゴリが指定されていないイシュー + +### リストビューフィルタリング + +カンバンビューと同じフィルタ機能を提供します。 + +#### フィルタ適用方法 + +1. 上部の**Date**または**Filter**ボタンをクリックします +2. 希望する条件を設定します +3. フィルタリングされた結果がカテゴリ別にグループ化されて表示されます + +#### 空のカテゴリ処理 + +フィルタリング結果にイシューがないカテゴリは自動的に非表示になります。 + +### リストビューソート + +各列ヘッダーをクリックしてソートできます。 + +#### ソート動作 + +- **最初のクリック**: 昇順ソート ↑ +- **2回目のクリック**: 降順ソート ↓ + +#### 各ソート内容 + +| ソート基準 | 使用例 | +| -------------------- | -------------------------------- | +| **Created ↓** | 最新イシューから確認 | +| **Feedback Count ↓** | 影響度が大きいイシュー優先処理 | +| **Status** | ステータス別にグループ化して確認 | + +--- + +## イシューカテゴリ + +イシューカテゴリは、イシューを分類して体系的に管理できるようにする機能です。 + +### カテゴリの目的 + +- **イシュー分類**: バグ、機能リクエスト、改善事項などに区分 +- **分析の容易性**: カテゴリ別のイシュー発生パターン分析 + +### デフォルトカテゴリ例 + +一般的に使用されるカテゴリ分類: + +| カテゴリ | 説明 | 優先度 | 担当チーム例 | +| ------------------ | ---------------------------- | ------ | -------------- | +| **バグ** | 機能誤動作、エラー | 高 | 開発チーム | +| **機能リクエスト** | 新機能追加リクエスト | 中 | 企画チーム | +| **改善事項** | 既存機能向上 | 中 | UXチーム | +| **パフォーマンス** | 速度、安定性問題 | 高 | インフラチーム | +| **UI/UX** | ユーザーインターフェース問題 | 低 | デザインチーム | +| **ドキュメント** | ヘルプ、ガイド関連 | 低 | 技術文書チーム | + +### カテゴリ管理 + +#### カテゴリの追加 + +イシュー詳細パネルで新しいカテゴリを追加できます: + +1. イシュー詳細パネルの**Category**フィールドで**Add**ボタンをクリックします +2. 新しいカテゴリ名を入力します +3. **Enter**キーを押すか確認ボタンをクリックします + +#### カテゴリの割り当て + +既存のイシューにカテゴリを割り当てたり変更したりできます: + +1. イシュー詳細パネルで**Edit**ボタンをクリックします +2. **Category**ドロップダウンから希望するカテゴリを選択します +3. **Save**ボタンで変更を保存します + +### カテゴリ別イシュー管理 + +#### リストビューでカテゴリ別確認 + +リストビューでは、カテゴリ別にグループ化されたイシューを一目で確認できます: + +- **カテゴリ別イシュー数**: 各グループヘッダーに含まれるイシュー数を表示 +- **グループ折りたたみ/展開**: 必要なカテゴリのみを選択的に確認 +- **"No Category"グループ**: 未分類イシューの別管理 + +#### カテゴリ別フィルタリング + +特定カテゴリのイシューのみを確認したい場合: + +1. **Filter**ボタンをクリックします +2. **Category**条件を追加します +3. 希望するカテゴリを選択します + +--- + +## 関連ドキュメント + +- [フィードバック管理](./04-feedback-management.md) - フィードバックからイシューを作成・リンクする方法 +- [イシュートラッカー統合](/ja/user-guide/settings/issue-tracker-management) - 外部ツールとの統合設定 +- [プロジェクト管理](./02-project-management.md) - チーム構成と権限管理 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/01-tenant-settings.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/01-tenant-settings.md new file mode 100644 index 000000000..53c1e1d99 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/01-tenant-settings.md @@ -0,0 +1,236 @@ +--- +title: テナント設定 +description: ABC User Feedbackのテナント情報、ログイン方式、ユーザー管理など、組織全体に影響を与える設定を管理する方法を案内します。 +sidebar_position: 1 +--- + +# テナント設定 + +テナント設定はABC User Feedbackの最上位管理機能で、組織全体に影響を与える重要な設定を扱います。このドキュメントでは、テナント情報管理、ログイン方式設定、全ユーザー管理方法を説明します。 + +> **注意**: これらの設定は**Super Admin権限**を持つユーザーのみがアクセスできます。 + +--- + +## テナント設定 + +テナントは組織の最上位単位で、すべてのプロジェクトとユーザーが含まれる範囲です。 + +### アクセス方法 + +1. 右上メニューで**Home**アイコンをクリックします +2. 左メニューから**Tenant Information**を選択します + +### 編集可能な項目 + +| 項目 | 説明 | 編集可能 | 例 | +| --------------- | ------------------------------------- | -------- | ----------------------- | +| **ID** | テナント固有識別子(システム自動生成) | ❌ 不可 | `1` | +| **Name** | テナント名(組織名、会社名など) | ✅ 可能 | `ABC Company` | +| **Description** | テナント説明(オプション) | ✅ 可能 | `顧客フィードバック管理システム` | + +### 情報編集方法 + +1. **Name**または**Description**フィールドを編集します +2. 右上の**Save**ボタンをクリックします +3. 保存完了時に成功メッセージが表示されます + +> テナント名はログインUIに表示される場合があります。 + +--- + +## ログイン設定 + +ユーザーがシステムにアクセスする際に使用する認証方式を設定します。 + +### アクセス方法 + +1. 右上メニューで**Home**アイコンをクリックします +2. 左メニューから**Login Management**を選択します + +### サポートされるログイン方式 + +#### 1. メールログイン + +デフォルトで提供されるメール+パスワードの組み合わせ方式です。 + +**特徴**: + +- 追加設定なしでデフォルト有効化 +- ユーザー招待 → メール認証 → パスワード設定の順序 +- パスワードリセット機能提供 + +**パスワードポリシー**: + +- 最低8文字以上 +- 英字、数字、特殊文字を含むことを推奨 +- 連続文字禁止(例:`aa`、`11`) + +#### 2. Googleログイン + +Google OAuth 2.0によるソーシャルログイン方式です。 + +**設定方法**: + +1. **Googleログイン有効化**: トグルをONに切り替えます +2. **Google Cloud Console設定が必要です**: + +> **参考**: Google OAuth統合の詳細な実装方法については、[OAuth統合ガイド](/ja/developer-guide/oauth-integration)を参照してください。 + +#### 3. カスタムOAuthログイン + +独自のOAuthサーバーや他のOAuthプロバイダーを使用する方式です。 + +**設定項目**: + +| 項目 | 説明 | 例 | +| ----------------- | ----------------------------- | ---------------------------------------- | +| **Provider Name** | ログインボタンに表示される名前 | `Microsoftでログイン` | +| **Client ID** | OAuthクライアントID | `abc123xyz` | +| **Client Secret** | OAuthクライアントシークレット | `supersecret` | +| **Auth URL** | 認証リクエストURL | `https://auth.example.com/oauth2/auth` | +| **Token URL** | トークンリクエストURL | `https://auth.example.com/oauth2/token` | +| **User Info URL** | ユーザー情報リクエストURL | `https://auth.example.com/oauth2/userinfo` | +| **Scope** | リクエストする権限範囲 | `openid email profile` | +| **Email Key** | ユーザー情報のメールフィールド名 | `email` | + +**設定順序**: + +1. 各フィールドにOAuthサーバー情報を入力します +2. **Save**ボタンをクリックして保存します +3. ログイン画面で設定されたProvider Nameでボタンが表示されます + +### ログイン方式の組み合わせ + +複数のログイン方式を同時に有効化できます: + +- **メールのみ**: デフォルトログインフォームのみ表示 +- **メール+Google**: ログインフォーム+「Googleでログイン」ボタン +- **メール+カスタム**: ログインフォーム+カスタムOAuthボタン + +### ログイン設定のテスト + +設定変更後、必ずテストを実施してください: + +1. ブラウザのシークレットモードでログインページにアクセス +2. 設定したログイン方式が正常に表示されるか確認 +3. 各方式で実際のログインテストを実行 + +--- + +## ユーザー管理 + +テナント全体のユーザーを中央で統合管理する機能です。 + +### アクセス方法 + +1. 右上メニューで**Home**アイコンをクリックします +2. 左メニューから**User Management**を選択します + +### ユーザーリストの確認 + +#### 表示される情報 + +| 列 | 説明 | 表示例 | +| ---------- | ------------------------- | ---------------------- | +| Email | ログインアカウントメールアドレス | `user@company.com` | +| Name | ユーザー名(プロフィール基準) | `山田太郎` | +| Department | 所属部門 | `開発チーム` | +| Type | ユーザータイプ | `SUPER` / `GENERAL` | +| Project | アクセス可能なプロジェクトリスト | `プロジェクトA、プロジェクトB` | +| Created | アカウント作成日時 | `2024-03-15 14:30` | + +#### ユーザータイプの説明 + +| タイプ | 説明 | 権限範囲 | +| --------- | -------------------------------------------------------- | --------------- | +| `SUPER` | すべてのプロジェクトと設定にアクセス可能。全体システム管理者の役割 | テナント全体 | +| `GENERAL` | 指定されたプロジェクトにのみアクセス可能 | 特定のプロジェクトのみ | + +### ユーザー検索とフィルタリング + +大量のユーザーがいる場合、希望するユーザーを素早く見つけることができます。 + +#### フィルタ機能 + +上部の**Filter**ボタンをクリックして条件を設定します。 + +**フィルタ条件**: + +- **Email**: メールアドレスで検索 +- **Name**: ユーザー名で検索 +- **Department**: 部門名で検索 + +**演算子オプション**: + +- **CONTAINS**: 含む場合 +- **IS**: 完全一致する場合 + +### ユーザーの招待 + +新しいユーザーをシステムに招待します。 + +#### 招待方法 + +1. 右上の**Invite User**ボタンをクリックします +2. 招待情報を入力します + +| 項目 | 説明 | オプション | +| ----------- | --------------------------- | ----------------------------- | +| **Email** | 招待するユーザーのメールアドレス | 必須入力 | +| **Type** | ユーザータイプ | `GENERAL` / `SUPER` | +| **Project** | アクセスを許可するプロジェクト | プロジェクトリストから選択 | +| **Role** | そのプロジェクトでの役割 | `Admin` / `Editor` / `Viewer` | + +3. **Invite**ボタンをクリックして招待を完了します + +#### 招待後のプロセス + +1. 招待されたユーザーにメールが送信されます +2. ユーザーがメールのリンクをクリックして登録手続きを進めます +3. 登録完了後、指定されたプロジェクトに自動的に追加されます + +### ユーザー情報の編集 + +既存のユーザーの情報と権限を変更できます。 + +#### 編集方法 + +1. ユーザーリストで編集したいユーザーをクリックします +2. **Edit User**ポップアップが開きます + +#### 編集可能な項目 + +| 項目 | 編集可能 | 説明 | +| --------- | -------- | ------------------------------ | +| **Email** | ❌ 不可 | アカウント識別子として変更不可 | +| **Type** | ✅ 可能 | `GENERAL` ↔ `SUPER`変更可能 | + +#### 保存と適用 + +1. 必要な情報を編集します +2. **Save**ボタンをクリックします +3. 変更は即座に適用され、該当ユーザーの次回ログインから反映されます + +### ユーザーの削除 + +システムを使用しなくなったユーザーを削除できます。 + +#### 削除方法 + +1. ユーザー編集ポップアップ下部の**Delete**ボタンをクリックします +2. 確認ダイアログで削除を承認します + +#### 削除時の注意事項 + +- **復元不可能**: 削除されたユーザーアカウントは元に戻せません +- **アクセス権限の即座削除**: 削除時、すべてのシステムアクセスが即座にブロックされます + +--- + +## 関連ドキュメント + +- [プロジェクト管理](../02-project-management.md) - プロジェクト別メンバーと権限管理 +- [OAuth統合ガイド](../../02-developer-guide/03-oauth-integration.md) - OAuth設定の技術的実装方法 +- [API統合](/ja/developer-guide/api-integration) - APIによるユーザー管理方法 + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/02-api-key-management.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/02-api-key-management.md new file mode 100644 index 000000000..cc9b1e9bf --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/02-api-key-management.md @@ -0,0 +1,134 @@ +--- +title: APIキー設定 +description: ABC User Feedbackで外部システム統合のためのAPIキーを作成、管理し、セキュリティを維持する方法を説明します。 +sidebar_position: 2 +--- + +# APIキー設定 + +APIキーは、外部システムがABC User Feedbackと安全に統合できるようにする認証手段です。このドキュメントでは、APIキーの作成から管理、セキュリティ維持まで、画面中心で説明します。 + +![api-key-setting.png](/img/api-key/api-key-setting.png) + +--- + +## APIキーの概要 + +### APIキーの役割 + +APIキーは次の目的で使用されます: + +- **外部システム認証**: ウェブサイト、モバイルアプリからフィードバック送信 +- **自動化統合**: バッチジョブ、スクリプトによるデータ収集 +- **サードパーティツール接続**: 分析ツール、モニタリングシステム統合 +- **セキュリティ制御**: プロジェクト別の独立したアクセス権限管理 + +### セキュリティ特徴 + +- **プロジェクト別独立性**: 各プロジェクトごとに別々のキー発行 +- **ステータス管理**: Active/Inactiveステータスで即座に制御可能 + +--- + +## APIキーの作成 + +### アクセス方法 + +1. 上部メニューで**Settings**をクリック +2. 左メニューから**APIキー管理**を選択 + +### キー作成プロセス + +#### 1. 作成ボタンをクリック + +APIキー管理画面で右上の**Create API Key**ボタンをクリックします。 + +#### 2. 自動生成と表示 + +ボタンをクリックするとすぐに新しいAPIキーが自動生成され、ポップアップで表示されます。 + +**ポップアップ構成要素**: + +- **APIキー値**: 完全なキー文字列を表示 +- **Copyボタン**: クリップボードに即座にコピー + +--- + +## APIキーリスト管理 + +### キーリスト画面構成 + +作成されたAPIキーはテーブル形式で管理されます。 + +#### テーブル列情報 + +| 列 | 説明 | 表示形式 | +| ----------- | ---------------------- | -------------------------- | +| **APIキー** | 生成されたキー値 | `AbcdEfgh...` | +| **Status** | 現在の有効化ステータス | Active / Inactive | +| **Created** | キー作成日時 | `2024-03-15 14:30` | +| **Actions** | 管理アクションボタン | ステータス変更、削除ボタン | + +#### キー識別方法 + +完全なキー値を再度確認できないため、次の方法でキーを区別します: + +- **作成時刻**: いつ作成されたキーか確認 +- **使用目的メモ**: 別途キーの用途を記録しておく + +--- + +## APIキーステータス管理 + +![api-key-detail.png](/img/api-key/api-key-detail.png) + +### Active / Inactiveの切り替え + +各APIキーは即座に有効化/無効化できます。 + +#### ステータス別の意味 + +| ステータス | 説明 | API呼び出し結果 | +| ------------ | --------------------------- | ---------------- | +| **Active** | 実際のAPI呼び出しに使用可能 | 正常処理 | +| **Inactive** | 呼び出しブロック状態 | 401 Unauthorized | + +#### ステータス変更方法 + +1. APIキーリストで**Status**列のトグルスイッチをクリックします +2. ステータスが即座に変更され、画面に反映されます +3. そのキーを使用する外部システムで即座に影響を受けます + +--- + +## APIキーの削除 + +### 削除タイミング + +次の場合、APIキーを削除する必要があります: + +- **キー公開**: 誤ってキーが公開された場合 +- **プロジェクト終了**: 該当プロジェクトの使用終了 +- **セキュリティポリシー**: 定期的なキー交換ポリシーに従って +- **未使用キー**: 使用しなくなったキーの整理 + +### 削除方法 + +#### 1. 削除ボタンをクリック + +キーリストで削除したいキーの**Actions**列の削除ボタンをクリックします。 + +#### 2. 削除確認 + +確認ダイアログで削除を最終承認します。 + +#### 3. 削除完了 + +**Delete**ボタンをクリックすると、キーが即座に削除され、リストから削除されます。 + +--- + +## 関連ドキュメント + +- [API統合ガイド](/ja/developer-guide/api-integration) - APIキーを使用した実際の統合実装方法 +- [プロジェクト管理](/ja/user-guide/project-management) - プロジェクト別APIキー管理 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/03-issue-tracker-management.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/03-issue-tracker-management.md new file mode 100644 index 000000000..dcf6a07c9 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/03-issue-tracker-management.md @@ -0,0 +1,167 @@ +--- +title: イシュートラッカー設定 +description: ABC User Feedbackで外部イシュートラッカー(Jiraなど)と統合してイシューを追跡し、リンクする方法を案内します。 +sidebar_position: 3 +--- + +# イシュートラッカー設定 + +イシュートラッカー設定により、ABC User Feedbackのイシューを外部イシュー管理システム(Jiraなど)と統合できます。内部イシューに外部チケットリンクを接続して、開発ワークフローと自然に統合できます。 + +--- + +## イシュートラッカーの概要 + +### 統合の目的 + +イシュートラッカー統合は次の目的で使用されます: + +- **ワークフロー統合**: 顧客フィードバックと開発作業の接続 +- **イシュー追跡**: 内部イシューと外部チケットの1対1マッピング +- **進行状況共有**: 開発チームとカスタマーサポートチーム間の情報同期 +- **効率性向上**: 重複作業の防止とコンテキストの維持 + +### 統合方式 + +- **手動リンク接続**: ABCイシューに外部チケット番号を手動で入力 +- **URL自動生成**: 設定されたBase URLとProject Keyで自動リンク生成 +- **クリック移動**: 生成されたリンクをクリックして外部システムにすぐに移動 + +> **参考**: リアルタイム双方向同期はサポートしていません。ステータス変更などの同期が必要な場合は、[Webhook](./04-webhook-management.md)を活用してください。 + +--- + +## 設定画面へのアクセス + +### アクセス方法 + +1. 上部メニューで**Settings**をクリック +2. 左メニューから**イシュートラッカー管理**を選択 + +### 設定画面構成 + +イシュートラッカー管理画面は次のように構成されます: + +- **Issue Tracking System**: 統合するシステムを選択するドロップダウン +- **Connection Settings**: 接続情報入力エリア +- **Link Preview**: 生成されるリンクのプレビュー +- **Test Connection**: 接続テストボタン + +--- + +## Jira統合設定 + +### システム選択 + +#### 1. Issue Tracking System設定 + +ドロップダウンから**Jira**を選択します。 + +### 接続情報入力 + +#### 2. Base URL設定 + +Jiraシステムの基本アドレスを入力します。 + +| 入力例 | 説明 | +| ----------------------------------- | ----------------------- | +| `https://yourcompany.atlassian.net` | Jira Cloudインスタンス | +| `https://jira.company.com` | セルフホスティングJira Server | +| `https://jira.internal:8080` | 内部ネットワークJira | + +**注意事項**: + +- `https://`または`http://`プロトコルを含める必要があります +- 最後のスラッシュ(`/`)を削除 +- ポート番号がある場合は含める + +#### 3. Project Key設定 + +Jiraプロジェクトの固有キーを入力します。 + +| 入力例 | 説明 | +| --------- | -------------------- | +| `PROJ` | 一般的なプロジェクトキー | +| `DEV` | 開発チームプロジェクト | +| `CS` | カスタマーサポートチームプロジェクト | +| `BUG` | バグ管理専用 | + +**Project Key確認方法**: + +1. Jiraで該当プロジェクトにアクセス +2. イシュー番号の`-`の前の部分がProject Key +3. 例:`PROJ-123`で`PROJ`がProject Key + +### リンクプレビュー + +設定した情報に基づいて生成されるリンクをプレビューできます。 + +#### プレビュー構成 + +``` +Base URL + /browse/ + Project Key + - + Issue Number +``` + +**例**: + +- Base URL: `https://yourcompany.atlassian.net` +- Project Key: `PROJ` +- Issue Number: `123`(ユーザーが入力) +- **生成されたリンク**: `https://yourcompany.atlassian.net/browse/PROJ-123` + +#### リンク形式検証 + +プレビューで次を確認してください: + +- URL形式が正しいか +- 実際のJiraイシューにアクセス可能か +- チームメンバーがアクセス権限を持っているか + +--- + +## イシューでのチケット接続 + +### チケット番号入力 + +イシュートラッカー設定が完了すると、個別のイシューで外部チケットを接続できます。 + +#### 接続方法 + +1. **Issue**タブで希望するイシューをクリックします +2. 右側の**Issue Details**パネルが開きます +3. **Ticket**フィールドに外部チケット番号を入力します + +#### 入力形式 + +| 入力方式 | 説明 | 生成されるリンク | +| --------- | ----------- | ------------------------------------------ | +| `123` | 数字のみ入力 | `https://jira.company.com/browse/PROJ-123` | + +システムが自動的にProject Keyを追加するため、数字のみ入力すれば十分です。 + +### リンク自動生成 + +チケット番号入力後、**Save**ボタンをクリックするとリンクが自動生成されます。 + +#### リンク機能 + +- **クリック移動**: リンクをクリックすると新しいタブで外部Jiraイシューに移動 +- **外部リンク表示**: リンクの横に外部リンクアイコンを表示 +- **編集可能**: Editモードでチケット番号を変更可能 + +### 接続解除 + +外部チケット接続を削除するには: + +1. イシュー詳細パネルで**Edit**ボタンをクリック +2. **Ticket**フィールドの内容を削除 +3. **Save**ボタンで変更を保存 + +--- + +## 関連ドキュメント + +- [イシュー管理](/ja/user-guide/issue-management) - イシュー作成とチケット接続の使用方法 +- [Webhook管理](/ja/user-guide/settings/webhook-management) - イシューステータス変更時の外部システム通知 +- [API統合ガイド](/ja/developer-guide/api-integration) - APIによるイシュー管理 + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/04-webhook-management.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/04-webhook-management.md new file mode 100644 index 000000000..a304959d5 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/04-webhook-management.md @@ -0,0 +1,170 @@ +--- +sidebar_position: 4 +title: 'ウェブフック設定' +description: '外部システムと自動統合するためにウェブフックを設定し、イベント発生時に通知を送信する方法を説明します。' +--- + +# ウェブフック設定 + +ウェブフックは、ABC User Feedbackで**特定のイベント発生時に外部システムに自動通知**を送信する機能です。フィードバック作成、イシューステータス変更などのイベントをリアルタイムで外部サービス(Slack、Discord、独自サーバーなど)に配信できます。詳細な統合ガイドについては、[ウェブフック統合](/ja/developer-guide/webhook-integration)ドキュメントを参照してください。 + +![webhook-setting](/img/webhook/webhook-setting.png) + +--- + +## アクセス方法 + +1. 上部メニューで**Settings**をクリック +2. 左メニューから**Webhook Integration**を選択 + +--- + +## Webhook Integration画面の概要 + +![webhook-list.png](/img/webhook/webhook-list.png) + +ウェブフック統合画面は次のように構成されます: + +### ウェブフックリストテーブル構成 + +| 列 | 説明 | +| ----------------- | --------------------------------------- | +| **On/Off** | ウェブフック有効化/無効化トグルスイッチ | +| **Name** | ウェブフック名 | +| **URL** | 通知を受信する外部エンドポイント | +| **Event Trigger** | 購読中のイベントトリガー | +| **Created** | ウェブフック作成日時 | + +--- + +## 新しいウェブフックの作成 + +### 1. ウェブフック登録開始 + +右上の**Register Webhook**ボタンをクリックすると、ウェブフック登録モーダルが開きます。 + +### 2. 基本情報入力 + +#### 必須入力項目 + +| 項目 | 説明 | +| -------- | ------------------------------------------- | +| **Name** | ウェブフック識別のための名前 | +| **URL** | HTTP POSTリクエストを受信するエンドポイント | + +### 3. トークン設定(オプション) + +**Token**フィールドで認証用のトークンを設定できます: + +- **Generate**ボタンをクリックして自動生成 +- または直接トークン値を入力 + +### 4. Event Trigger選択 + +購読するイベントをチャネル別に選択できます: + +#### サポートされるイベントタイプ + +各チャネル(VOC、Review、Survey、VOC Test)について、次のイベントを選択できます: + +| イベントタイプ | 説明 | +| ----------------------- | ------------------------------------------ | +| **Feedback Creation** | 新しいフィードバックが登録されたとき | +| **Issue Registration** | 新しいイシューが作成されたとき | +| **Issue Status Change** | イシューステータスが変更されたとき | +| **Issue Creation** | イシューがフィードバックにリンクされたとき | + +### 5. ウェブフック保存 + +すべての情報を入力した後: + +1. **OK**ボタンをクリックしてウェブフックを作成 +2. **Cancel**ボタンでキャンセル可能 + +--- + +## ウェブフックステータス管理 + +### 有効化/無効化の切り替え + +ウェブフックリストで各ウェブフックの**On/Off**列にあるトグルスイッチをクリックしてステータスを変更できます: + +- **On(有効化)**: イベント発生時にリアルタイム送信 +- **Off(無効化)**: ウェブフックは維持されますが送信は停止 + +### 一時無効化シナリオ + +- 外部サーバーがメンテナンス中の場合 +- ウェブフックURL変更作業中の場合 +- スパム通知を防ぐ必要がある場合 + +--- + +## ウェブフック編集と削除 + +### ウェブフック編集 + +ウェブフックリストで編集したいウェブフックをクリックすると編集モーダルが開きます: + +#### 編集可能な項目 + +- ウェブフック名 +- Target URL変更 +- トークン値修正 +- イベントタイプ追加/削除 + +### ウェブフック削除 + +ウェブフックを完全に削除するには: + +1. 編集モーダルで削除オプションを選択 +2. またはリストから直接削除ボタンをクリック(UIに削除ボタンがある場合) + +--- + +## ウェブフックテストと検証 + +### 手動検証方法 + +1. **フィードバック作成テスト**: + - テストフィードバックを登録して`Feedback Creation`イベントを確認 +2. **イシュー管理テスト**: + - イシューを作成またはステータスを変更して関連イベントを確認 +3. **外部サービス確認**: + - Slack、Discordなどでメッセージ受信を確認 + +--- + +## 一般的な統合例 + +### Slackウェブフック設定 + +``` +Name: Slack通知 +URL: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX +Events: Feedback Creation, Issue Registration(すべてのチャネル) +``` + +### Discordウェブフック設定 + +``` +Name: Discord開発チーム通知 +URL: https://discord.com/api/webhooks/123456789/abcdefghijk +Events: Issue Status Change(VOCチャネルのみ) +``` + +### カスタムサーバー統合 + +``` +Name: 内部分析システム +URL: https://api.yourcompany.com/webhooks/feedback +Token: your-generated-token +Events: すべてのイベント(すべてのチャネル) +``` + +--- + +## 関連ドキュメント + +- [Webhook開発者ガイド](/ja/developer-guide/webhook-integration) - ウェブフック受信サーバーの実装方法 +- [APIキー管理](./02-api-key-management.md) - APIキーベース認証設定 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/05-ai-setting.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/05-ai-setting.md new file mode 100644 index 000000000..208e86dfe --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/05-ai-setting.md @@ -0,0 +1,272 @@ +--- +sidebar_position: 5 +title: 'AI設定' +description: '生成AI機能を使用するための基本設定と統合方法を説明します。' +--- + +# AI設定 + +ABC User Feedbackで**生成AI機能**を使用するには、まずAIプロバイダーとの統合を設定する必要があります。 +AI設定を完了すると、**AIフィールドテンプレート**、**AIイシュー推奨**、**AI使用量モニタリング**などのすべての機能を活用できます。 + +--- + +## アクセス方法 + +1. 上部メニューで**Settings**をクリック +2. 左メニューから**Generative AI Integration**を選択 +3. 上部タブで**AI Setting**をクリック + +--- + +## AIプロバイダー選択と設定 + +![ai-setting.png](/img/ai/ai-setting.png) + +### 1. プロバイダー選択 + +現在サポートされているAIプロバイダーOpenAI、Google Geminiのいずれかを選択します: + +### 2. APIキー入力 + +選択したAIプロバイダーから発行されたAPIキーを入力します。 + +### 3. Base URL設定(オプション) + +ほとんどの場合、**空欄にしておくとデフォルト値**が自動的に使用されます。 + +特別なエンドポイントやプロキシサーバーを使用する場合のみ入力してください。 + +### 4. System Prompt設定(オプション) + +AIがすべてのリクエストを処理する際に参照する**基本指示**を設定できます。 + +組織のトーンアンドマナーや特別な要件がある場合に活用してください。 + +### 5. 設定保存 + +すべての情報を入力した後、右上の**Save**ボタンをクリックします。 + +--- + +## AI使用量モニタリング + +![ai-usage.png](/img/ai/ai-usage.png) + +**AI Usage**タブでAI機能の使用量とコストをモニタリングできます。 + +### 使用量ダッシュボード + +確認できる情報: + +- **日次/月次API呼び出し数** +- **トークン使用量**(入力/出力別) +- **機能別使用分布**(AIフィールド vs イシュー推奨) + +--- + +## AIフィールドテンプレート管理 + +![ai-field-template.png](/img/ai/ai-field-template.png) + +AI設定を完了した後、**AI Field Template**タブでフィードバック自動分析テンプレートを管理できます。 + +### デフォルトテンプレート + +システムで提供されるデフォルトテンプレート: + +| テンプレート | 説明 | 活用例 | +| ---------------------- | ---------------------------------------------- | ------------------------------ | +| **Feedback Summary** | フィードバックを1文で要約 | 長いフィードバックの核心を把握 | +| **Sentiment Analysis** | 感情分析(ポジティブ/ネガティブ/ニュートラル) | 顧客満足度トレンド分析 | +| **Translation** | フィードバックを英語に翻訳 | 多言語フィードバック統合分析 | +| **Keyword Extraction** | 核心キーワード2-3個抽出 | イシューカテゴリ自動タグ付け | + +### カスタムテンプレート作成 + +![ai-field-template-create.png](/img/ai/ai-field-template-create.png) + +1. **Create New**カードをクリック +2. テンプレート情報を入力 + +| 項目 | 説明 | +| --------------- | --------------------- | +| **Title** | テンプレート名 | +| **Prompt** | AIに与える指示文 | +| **Model** | 使用するAIモデル選択 | +| **Temperature** | 創造性調整(0.0~1.0) | + +3. Playgroundでテスト + +- "Add Data"ボタンでテストフィードバック入力 +- "AI test execution"クリックで結果確認 + +### テンプレート編集と削除 + +- テンプレートカードクリック → 編集 +- **Delete Template**ボタンで削除 +- 削除時、そのテンプレートを使用するAIフィールドに影響を与える可能性があります + +--- + +## AIフィールドをチャネルに適用 + +AIフィールドテンプレートを作成した後、実際のチャネルのフィールドとして適用する必要があります。フィードバックでAI分析結果を確認できます。 + +### 1. Field ManagementでAIフィールド追加 + +**Settings > Channel List > [チャネル選択] > Field Management**でAIフィールドを追加します。 + +#### AIフィールド設定項目 + +| 項目 | 説明 | 必須 | +| ----------------------- | ------------------------------------ | ---------- | +| **Key** | フィールド固有識別子 | 必須 | +| **Display Name** | UIに表示される名前 | 必須 | +| **Format** | `aiField`選択 | 必須 | +| **Template** | 作成したAIフィールドテンプレート選択 | 必須 | +| **Target Field** | 分析対象となるテキストフィールド | 必須 | +| **Property** | EditableまたはRead Only | 必須 | +| **AI Field Automation** | 自動実行有無 | オプション | + +#### 設定例 + +``` +Key: sentiment_analysis +Display Name: 感情分析 +Format: aiField +Template: Feedback Sentiment Analysis +Target Field: message +Property: Read Only +AI Field Automation: ON(自動実行) +``` + +### 2. Template接続とTarget Field設定 + +**Template**ドロップダウンから以前に作成したAIフィールドテンプレートを選択します。 + +**Target Field**はAI分析の対象となるフィールドを指定します + +### 3. AI Field Automation設定 + +**AI Field Automation**トグルを通じて実行方式を選択します: + +- **ON(自動実行)**: 新しいフィードバック登録時に自動的にAI分析実行 +- **OFF(手動実行)**: ユーザーが手動で実行ボタンをクリックする必要があります + +## フィードバックでAI分析結果確認 + +AIフィールド設定が完了すると、フィードバックリストと詳細画面でAI分析結果を確認できます。 + +### フィードバックリストで確認 + +フィードバックテーブルにAIフィールドが新しい列として追加されます: + +- **Summary**: AIが生成した要約 +- **Classification**: AI分類結果 +- **Korean**: 翻訳結果など + +### フィードバック詳細画面で確認 + +フィードバック詳細表示パネルでより詳細なAI分析結果を確認できます: + +1. フィードバック行をクリック → 右側詳細パネルが開きます +2. AIフィールド別の分析結果を確認 +3. 各AIフィールドごとに分析結果とともに表示 + +## AI分析手動実行 + +フィードバック詳細画面で手動でAI分析を実行できます。 + +### Run AIボタン使用 + +1. フィードバック詳細画面で**Run AI**ボタンをクリック +2. AI分析が実行され、結果がそのフィールドに自動入力されます +3. 分析完了後、結果をすぐに確認可能 + +### 手動実行活用シナリオ + +- **コスト節約**: 必要なフィードバックのみを選択してAI分析 +- **パフォーマンス確認**: 新しいテンプレートの結果を事前にテスト +- **再分析**: テンプレート修正後、既存フィードバックを再分析 + +--- + +## AIイシュー推奨設定 + +**AI Issue Recommendation**タブでフィードバックベースの自動イシュー推奨機能を設定できます。 + +![ai-issue-recommendation.png](/img/ai/ai-issue-recommendation.png) + +### 推奨設定作成 + +![ai-issue-recommendation-create.png](/img/ai/ai-issue-recommendation-create.png) + +1. **Create New**ボタンをクリック +2. 設定項目を入力 + +| 項目 | 説明 | 必須 | +| ---------------- | --------------------------------- | ---------- | +| **Channel** | 適用するチャネル選択 | 必須 | +| **Target Field** | 分析対象フィールド(例:message) | 必須 | +| **Prompt** | 推奨基準プロンプト | オプション | +| **Enable** | 機能有効化トグル | 必須 | + +3. 高度な設定 + +| 設定 | 説明 | +| ------------------------- | -------------------------------------------------- | +| **Model** | 使用モデル | +| **Temperature** | 創造性調整 | +| **Data Reference Amount** | 参照するイシュー量(イシューと関連フィードバック) | + +### 推奨機能テスト + +入力された設定についてPlaygroundでテスト: + +1. 例示フィードバック入力 +2. "AI test execution"クリック +3. 推奨イシューリスト確認 + +### 実際のフィードバックで推奨活用 + +フィードバック詳細表示で: + +- AI推奨イシューリスト確認 +- チェックボックスで適切なイシュー選択 +- **Retry**ボタンで別の推奨リクエスト + +### フィードバックリストでイシュー推奨使用 + +AIイシュー推奨を設定したチャネルでは、フィードバックリスト画面でも直接イシュー推奨機能を使用できます。 + +#### 使用方法 + +1. フィードバックリストでイシューを接続したいフィードバックの**Issue列**にある**+ボタン**をクリック +2. ドロップダウンメニューが表示されたら**"Run AI"**を選択 +3. AIが関連イシューを分析して推奨リストを表示 + +#### 推奨結果確認と適用 + +AI分析完了後、推奨イシューリストから: + +- **推奨されたイシュー**確認 +- 推奨された適切なイシュー選択 +- 新しいイシュー作成オプションも提供 +- 選択完了後、該当イシューがフィードバックに自動接続 + +#### 一括処理活用 + +複数のフィードバックを選択した状態でもAIイシュー推奨を使用でき、効率的なフィードバック分類が可能です: + +1. フィードバックリストで複数行選択(チェックボックス活用) +2. 上部一括作業メニューでAIイシュー推奨実行 +3. 各フィードバックごとに推奨イシュー確認と適用 + +--- + +## 関連ドキュメント + +- [フィールド設定](/ja/user-guide/feedback-management) - AIフィールドをチャネルに適用する方法 +- [イシュー作成とステータス管理](/ja/user-guide/issue-management) - AI推奨イシューの活用方法 +- [フィードバック確認とフィルタリング](/ja/user-guide/feedback-management) - AI分析結果確認方法 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/06-image-setting.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/06-image-setting.md new file mode 100644 index 000000000..d7d5bced1 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/06-image-setting.md @@ -0,0 +1,102 @@ +--- +sidebar_position: 6 +title: '画像設定' +description: 'フィードバックに添付された画像の保存方式とセキュリティポリシーを設定する方法を案内します。' +--- + +# 画像設定 + +ABC User Feedbackでは、ユーザーがフィードバックを提出する際に**画像と一緒にアップロード**できるようにサポートしています。画像の保存方式とセキュリティポリシーを適切に設定することで、安全で効率的なフィードバック収集環境を構築できます。 + +![image-setting.png](/img/image/image-setting.png) + +--- + +## アクセス方法 + +1. 上部メニューで**Settings**をクリック +2. 左メニューから**Channel List > [チャネル選択]** +3. 下部タブから**Image Management**を選択 + +--- + +## Image Storage Integration設定 + +**Multipart Upload API**方式で画像をサーバーに直接アップロードするか、**Presigned URL Download**機能を活用するには、S3またはS3互換ストレージ統合が必要です。 + +### 必須設定項目 + +| 項目 | 説明 | 例 | +| --------------------- | ---------------------------- | ----------------------------------------- | +| **Access Key ID** | S3アクセスのためのキーID | `AKIAIOSFODNN7EXAMPLE` | +| **Secret Access Key** | キーに対するシークレット | `wJalrXUtnFEMI/K7MDENG/...` | +| **End Point** | S3 APIエンドポイントURL | `https://s3.ap-northeast-1.amazonaws.com` | +| **Region** | バケットが位置する地域 | `ap-northeast-1` | +| **Bucket Name** | 画像が保存される対象バケット | `consumer-ufb-images` | + +### Presigned URL Download設定 + +**Presigned URL Download**オプションを通じて画像ダウンロードセキュリティを強化できます。 + +#### 設定オプション + +- **Enable**: 認証されたワンタイムURLを通じて画像にアクセス(セキュリティ強化) +- **Disable**: 画像URLが直接公開され、公開アクセス可能 + +### 接続テスト + +すべての設定を入力した後、**Test Connection**ボタンをクリックしてストレージ接続を確認します。 + +接続結果: + +- ✅ **成功**: "Connection test succeeded"メッセージ +- ❌ **失敗**: 入力値、バケット権限、ネットワーク設定を再確認する必要があります + +--- + +## Image URL Domain Whitelist設定 + +**Image URL方式**を使用するか、セキュリティを強化したい場合、信頼できるドメインのみを許可するようにホワイトリストを設定できます。 + +### 現在の状態確認 + +デフォルト設定は**"All image URLs are allowed"**状態で、すべてのドメインの画像URLを許可します。 + +### ホワイトリスト追加 + +セキュリティ強化のために特定のドメインのみを許可するには: + +1. **Whitelist**エリアに信頼できるドメインを追加 +2. 例示ドメイン: + - `cdn.yourcompany.com` + - `images.trusted-partner.io` + - `storage.googleapis.com` + +--- + +## サポートされるストレージサービス + +### AWS S3 + +- 最も一般的に使用されるクラウドストレージ +- 安定性が高く、拡張性に優れています + +--- + +## 設定保存 + +すべての設定を完了した後、右上の**Save**ボタンをクリックして変更を保存します。 + +保存後: + +- 新しい画像アップロードが設定した方式で動作 +- 既存の画像は既存設定のまま維持 +- Test Connectionで設定の正常動作を再確認することを推奨 + +--- + +## 関連ドキュメント + +- [フィールド設定](/ja/user-guide/feedback-management) - 画像フィールドをフィードバックフォームに追加する方法 +- [フィードバック確認とフィルタリング](/ja/user-guide/feedback-management) - アップロードされた画像をフィードバックで確認する方法 +- [APIキー管理](./02-api-key-management.md) - APIキーセキュリティ管理方法 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/_category_.json b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/_category_.json new file mode 100644 index 000000000..4bd660ea8 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 7, + "label": "設定", + "description": "設定に関するガイドです。" +} diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/index.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/index.md new file mode 100644 index 000000000..b1e39fc63 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/07-settings/index.md @@ -0,0 +1,7 @@ +--- +title: 設定 +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/_category_.json b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/_category_.json new file mode 100644 index 000000000..31ab24c0f --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 2, + "label": "ユーザーガイド" +} + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/index.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/index.md new file mode 100644 index 000000000..86f48e18c --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/01-user-guide/index.md @@ -0,0 +1,7 @@ +--- +title: ユーザーガイド +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/01-docker-hub-images.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/01-docker-hub-images.md new file mode 100644 index 000000000..6b0d86416 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/01-docker-hub-images.md @@ -0,0 +1,374 @@ +--- +id: docker-hub-images +title: Docker Hubイメージインストール +description: Docker Hubに登録されたABC User Feedback公式イメージを使用してシステムを迅速にインストールする方法を説明します。 +sidebar_position: 1 +--- + +# Docker Hubイメージインストール + +ABC User Feedbackは公式Dockerイメージを提供しています。 +このドキュメントは、Docker Composeを使用して**Web UI、APIサーバー、データベース、SMTPサーバー**などのシステムをローカルで迅速に構成する方法を説明します。 + +--- + +## 1. 前提条件 + +| 項目 | 説明 | +| -------------- | ----------------------------------------------------------------------- | +| Docker | 20.10以上 | +| Docker Compose | v2以上推奨 | +| 使用ポート | `3000`、`4000`、`13306`、`5080`、`25`(ローカルで空いている必要がある) | + +--- + +## 2. Dockerイメージ構成 + +| サービス名 | 説明 | Dockerイメージ名 | +| ------------------------ | ------------------------------- | ------------------------------------- | +| Web (Admin UI) | フロントエンドWeb UI(Next.js) | `line/abc-user-feedback-web` | +| API (Backend) | バックエンドサーバー(NestJS) | `line/abc-user-feedback-api` | +| MySQL | データベース | `mysql:8.0` | +| SMTP4Dev | ローカルテスト用メールサーバー | `rnwood/smtp4dev:v3` | +| (オプション)OpenSearch | 検索機能とAI分析精度向上用 | `opensearchproject/opensearch:2.16.0` | + +--- + +## 3. `docker-compose.yml`の例 + +```yaml +name: abc-user-feedback +services: + web: + image: line/abc-user-feedback-web:latest + environment: + - NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 + ports: + - 3000:3000 + depends_on: + - api + restart: unless-stopped + + api: + image: line/abc-user-feedback-api:latest + environment: + - JWT_SECRET=jwtsecretjwtsecretjwtsecret + - MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@mysql:3306/userfeedback + - SMTP_HOST=smtp4dev + - SMTP_PORT=25 + - SMTP_SENDER=user@feedback.com + # OpenSearchを使用する場合は以下のコメントを解除してください + # - OPENSEARCH_USE=true + # - OPENSEARCH_NODE=http://opensearch-node:9200 + ports: + - 4000:4000 + depends_on: + - mysql + restart: unless-stopped + + mysql: + image: mysql:8.0 + command: + [ + '--default-authentication-plugin=mysql_native_password', + '--collation-server=utf8mb4_bin', + ] + environment: + MYSQL_ROOT_PASSWORD: userfeedback + MYSQL_DATABASE: userfeedback + MYSQL_USER: userfeedback + MYSQL_PASSWORD: userfeedback + TZ: UTC + ports: + - 13306:3306 + volumes: + - mysql:/var/lib/mysql + restart: unless-stopped + + smtp4dev: + image: rnwood/smtp4dev:v3 + ports: + - 5080:80 + - 25:25 + - 143:143 + volumes: + - smtp4dev:/smtp4dev + restart: unless-stopped + + # OpenSearchを使用する場合は以下のコメントを解除してください + # opensearch-node: + # image: opensearchproject/opensearch:2.16.0 + # restart: unless-stopped + # environment: + # - cluster.name=opensearch-cluster + # - node.name=opensearch-node + # - discovery.type=single-node + # - bootstrap.memory_lock=true + # - 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m' + # - plugins.security.disabled=true + # - OPENSEARCH_INITIAL_ADMIN_PASSWORD=UserFeedback123!@# + # ulimits: + # memlock: + # soft: -1 + # hard: -1 + # nofile: + # soft: 65536 + # hard: 65536 + # volumes: + # - opensearch:/usr/share/opensearch/data + # ports: + # - 9200:9200 + # - 9600:9600 + +volumes: + mysql: + smtp4dev: + # opensearch: +``` + +--- + +## 4. 実行手順 + +### 4.1 Dockerイメージのダウンロードと実行 + +```bash +# Docker Composeですべてのサービスをバックグラウンドで実行 +docker compose up -d +``` + +### 4.2 実行状態の確認 + +```bash +# すべてのコンテナが正常に実行中か確認 +docker compose ps +``` + +### 4.3 サービスアクセスの確認 + +- **Webアプリケーション**: [http://localhost:3000](http://localhost:3000) +- **APIサーバー**: [http://localhost:4000](http://localhost:4000) +- **SMTPテストページ**: [http://localhost:5080](http://localhost:5080) +- **MySQLデータベース**: `localhost:13306`(ユーザー:`userfeedback`、パスワード:`userfeedback`) + +--- + +## 5. SMTP設定 + +デフォルトでは、この構成では`smtp4dev`を通じてメールをテストできます。 + +- **Webインターフェース**: [http://localhost:5080](http://localhost:5080) +- **SMTPポート**: `25` +- **IMAPポート**: `143` + +### SMTPテスト方法 + +1. Webアプリケーションでユーザー登録またはユーザー招待機能を使用 +2. [http://localhost:5080](http://localhost:5080)で送信されたメールを確認 +3. メール内容と添付ファイルなどをテスト + +> **重要**: 実際の本番環境では、必ず外部SMTPサーバー(例:Gmail、SendGrid、社内SMTPなど)と連携する必要があります。 + +## 6. インストール確認 + +### 6.1 Webアプリケーションアクセス確認 + +ブラウザで`http://localhost:3000`にアクセスし、以下を確認してください: + +- テナント作成画面が正常に表示されるか +- ページの読み込みが完了するか +- JavaScriptエラーがないか(ブラウザの開発者ツールで確認) + +### 6.2 APIサーバーステータス確認 + +```bash +# APIサーバーヘルスチェック +curl http://localhost:4000/api/health +``` + +予想される応答: + +```json +{ + "status": "ok", + "info": { + "database": { + "status": "up" + } + } +} +``` + +### 6.3 データベース接続確認 + +```bash +# MySQLコンテナに直接アクセスしてデータベースを確認 +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "SHOW DATABASES;" + +# テーブル作成確認 +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "USE userfeedback; SHOW TABLES;" +``` + +### 6.4 ログ確認 + +```bash +# すべてのサービスのログを確認 +docker compose logs + +# 特定のサービスのログのみ確認 +docker compose logs api +docker compose logs web +docker compose logs mysql +``` + +--- + +## 7. OpenSearch使用時の注意事項 + +OpenSearchは、検索機能とAI分析の精度を向上させるオプションコンポーネントです。 + +### 7.1 OpenSearch有効化方法 + +1. `docker-compose.yml`ファイルで`api`サービスの環境変数のコメントを解除: + +```yaml +- OPENSEARCH_USE=true +- OPENSEARCH_NODE=http://opensearch-node:9200 +``` + +2. `opensearch-node`サービスのコメントを解除 +3. `volumes:`セクションで`opensearch:`のコメントを解除 +4. ポート`9200`、`9600`がローカルで使用されていないことを確認 + +### 7.2 メモリ要件 + +> **注意**: OpenSearchは最低2GB以上のメモリを必要とします。メモリ不足の場合、コンテナが自動的に終了する可能性があります。 + +### 7.3 OpenSearchステータス確認 + +```bash +# OpenSearchクラスターステータス確認 +curl http://localhost:9200/_cluster/health + +# OpenSearchノード情報確認 +curl http://localhost:9200/_nodes + +# インデックス確認 +curl http://localhost:9200/_cat/indices +``` + +### 7.4 OpenSearch無効化 + +OpenSearchを使用しない場合は、`docker-compose.yml`で該当サービスと環境変数をコメントアウトします。 + +--- + +## 8. トラブルシューティング + +### 8.1 ポート競合の問題 + +**症状**: `docker compose up`実行時にポートバインディングエラーが発生 + +**解決方法**: + +```bash +# 使用中のポートを確認 +lsof -i :3000 # Webポート +lsof -i :4000 # APIポート +lsof -i :13306 # MySQLポート +lsof -i :5080 # SMTPポート + +# 該当ポートを使用しているプロセスを停止して再起動 +docker compose down +docker compose up -d +``` + +### 8.2 コンテナ起動失敗 + +**症状**: 一部のコンテナが起動しない、または継続的に再起動される + +**解決方法**: + +```bash +# コンテナステータス確認 +docker compose ps + +# 失敗したコンテナのログを確認 +docker compose logs [サービス名] + +# すべてのコンテナを停止して削除 +docker compose down + +# ボリュームも削除(データ損失に注意) +docker compose down -v + +# 再度起動 +docker compose up -d +``` + +### 8.3 データベース接続エラー + +**症状**: APIサーバーからMySQL接続失敗 + +**解決方法**: + +```bash +# MySQLコンテナが完全に起動するまで待機 +docker compose logs mysql + +# MySQLコンテナに直接接続テスト +docker compose exec mysql mysql -u userfeedback -puserfeedback -e "SELECT 1;" + +# APIサービスを再起動 +docker compose restart api +``` + +### 8.4 イメージダウンロード失敗 + +**症状**: Dockerイメージをダウンロードできない + +**解決方法**: + +```bash +# Docker Hubログイン確認 +docker login + +# イメージを手動でダウンロード +docker pull line/abc-user-feedback-web:latest +docker pull line/abc-user-feedback-api:latest + +# ネットワーク接続確認 +ping hub.docker.com +``` + +### 8.5 メモリ不足の問題 + +**症状**: OpenSearchコンテナが自動的に終了する + +**解決方法**: + +```bash +# システムメモリ確認 +free -h + +# Dockerメモリ使用量確認 +docker stats + +# OpenSearchを無効化(docker-compose.ymlでコメントアウト) +# またはメモリ割り当てを増やす +``` + +--- + +## 9. 参考リンク + +- [ABC User Feedback Web - Docker Hub](https://hub.docker.com/r/line/abc-user-feedback-web) +- [ABC User Feedback API - Docker Hub](https://hub.docker.com/r/line/abc-user-feedback-api) +- [smtp4dev - Docker Hub](https://hub.docker.com/r/rnwood/smtp4dev) +- [OpenSearch - Docker Hub](https://hub.docker.com/r/opensearchproject/opensearch) + +--- + +## 関連ドキュメント + +- [初期設定ガイド](/ja/user-guide/getting-started) diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/02-cli-tool.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/02-cli-tool.md new file mode 100644 index 000000000..0bd20621e --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/02-cli-tool.md @@ -0,0 +1,273 @@ +--- +sidebar_position: 2 +title: "CLIツール使用方法" +description: "ABC User Feedback CLIツールでシステムを迅速かつ簡単にインストール・管理する方法を説明します。" +--- + +# CLIツール使用方法 + +ABC User Feedback CLI(`auf-cli`)は、システムのインストール、実行、管理を簡素化するコマンドラインツールです。Node.jsとDockerがインストールされていれば、追加の依存関係をインストールしたりリポジトリをクローンしたりすることなく、`npx`を通じてすぐに実行できます。 + +## 主要機能 + +- 必要なインフラの自動設定(MySQL、SMTP、OpenSearch) +- 環境変数設定の簡素化 +- APIおよびウェブサーバーの自動起動/停止 +- ボリュームデータのクリーンアップ +- 動的Docker Composeファイル生成 + +## 使用されるDockerイメージ + +- `line/abc-user-feedback-web:latest` - ウェブフロントエンド +- `line/abc-user-feedback-api:latest` - APIバックエンド +- `mysql:8.0` - データベース +- `rnwood/smtp4dev:v3` - SMTPテストサーバー +- `opensearchproject/opensearch:2.16.0` - 検索エンジン(オプション) + +## 前提条件 + +CLIツールを使用する前に、次の要件を満たす必要があります: + +- [Node.js v22以上](https://nodejs.org/en/download/) +- [Docker](https://docs.docker.com/desktop/) + +## 基本コマンド + +### 初期化 + +ABC User Feedbackに必要なインフラを設定するには、次のコマンドを実行します: + +```bash +npx auf-cli init +``` + +このコマンドは次の作業を実行します: + +1. 環境変数設定用の`config.toml`ファイルを作成 +2. アーキテクチャ(ARM/AMD)に応じて必要なインフラを設定 + +初期化が完了すると、現在のディレクトリに`config.toml`ファイルが作成されます。必要に応じてこのファイルを編集して環境変数を調整できます。 + +### サーバー起動 + +APIおよびウェブサーバーを起動するには、次のコマンドを実行します: + +```bash +npx auf-cli start +``` + +このコマンドは次の作業を実行します: + +1. `config.toml`ファイルから環境変数を読み取り +2. Docker Composeファイルを生成してサービスを開始 +3. APIおよびウェブサーバーコンテナと必要なインフラ(MySQL、SMTP、OpenSearch)を起動 + +サーバーが正常に起動すると、ウェブブラウザで`http://localhost:3000`(または設定されたURL)からABC User Feedbackウェブインターフェースにアクセスできます。CLIは次のURLを表示します: + +- ウェブインターフェースURL +- API URL +- MySQL接続文字列 +- OpenSearch URL(有効な場合) +- SMTPウェブインターフェース(smtp4dev使用時) + +### サーバー停止 + +APIおよびウェブサーバーを停止するには、次のコマンドを実行します: + +```bash +npx auf-cli stop +``` + +このコマンドは実行中のAPIおよびウェブサーバーコンテナとインフラコンテナを停止します。ボリュームに保存されたすべてのデータは保持されます。 + +### ボリュームクリーンアップ + +起動中に作成されたDockerボリュームをクリーンアップするには、次のコマンドを実行します: + +```bash +npx auf-cli clean +``` + +このコマンドはすべてのコンテナを停止し、MySQL、SMTP、OpenSearchなどのDockerボリュームを削除します。 + +**警告**: この操作はすべてのデータを削除するため、必要な場合は事前にバックアップしてください。 + +`--images`オプションを使用して未使用のDockerイメージもクリーンアップできます: + +```bash +npx auf-cli clean --images +``` + +## 設定ファイル(config.toml) + +`init`コマンドを実行すると、現在のディレクトリに`config.toml`ファイルが作成されます。このファイルはABC User Feedbackの環境変数を設定するために使用されます。 + +以下は`config.toml`ファイルの例です: + +```toml +[web] +port = 3000 +# api_base_url = "http://localhost:4000" + +[api] +port = 4000 +jwt_secret = "jwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecret" + +# master_api_key = "MASTER_KEY" +# access_token_expired_time = "10m" +# refresh_token_expired_time = "1h" + +# [api.auto_feedback_deletion] +# enabled = true +# period_days = 365 + +# [api.smtp] +# host = "smtp4dev" # SMTP_HOST +# port = 25 # SMTP_PORT +# sender = "user@feedback.com" +# username= +# password= +# tls= +# cipher_spec= +# opportunitic_tls= + +# [api.opensearch] +# enabled = true + +[mysql] +port = 13306 +``` + +必要に応じてこのファイルを編集して環境変数を調整できます。環境変数の詳細については、[環境変数設定](./05-configuration.md)ドキュメントを参照してください。 + +## 高度な使用方法 + +### ポート変更 + +デフォルトでは、ウェブサーバーはポート3000を、APIサーバーはポート4000を使用します。これらを変更するには、`config.toml`ファイルで次の設定を変更します: + +```toml +[web] +port = 8000 # ウェブサーバーポート変更 +api_base_url = "http://localhost:8080" # API URLも一緒に変更する必要があります + +[api] +port = 8080 # APIサーバーポート変更 + +[mysql] +port = 13307 # 必要に応じてMySQLポート変更 +``` + +### OpenSearch有効化 + +高度な検索機能のためにOpenSearchを有効にするには: + +```toml +[api.opensearch] +enabled = true +``` + +**注意事項**: + +- OpenSearchには最低2GBの使用可能メモリが必要です +- OpenSearchコンテナは`http://localhost:9200`で利用可能です +- OpenSearchステータス確認: `http://localhost:9200/_cluster/health` + +### SMTP設定 + +開発環境では、デフォルトの`smtp4dev`設定を推奨します: + +```toml +[api.smtp] +host = "smtp4dev" +port = 25 +sender = "dev@feedback.local" +``` + +smtp4devウェブインターフェースは`http://localhost:5080`で送信されたメールを確認できます。 + +## トラブルシューティング + +### 一般的な問題 + +1. **Docker関連エラー**: + + - Dockerが実行中か確認: `docker --version` + - Docker権限確認: `docker ps` + - Docker Desktopが正しくインストールされ実行中か確認 + +2. **ポート競合**: + + - ポート使用確認: `lsof -i :PORT`(macOS/Linux)または`netstat -ano | findstr :PORT`(Windows) + - `config.toml`でポート設定変更 + - 一般的な競合ポート: 3000、4000、13306、9200、5080 + +3. **サービス起動失敗**: + + - コンテナログ確認: `docker compose logs SERVICE_NAME` + - Dockerイメージが利用可能か確認: `docker images` + - 十分なシステムリソース(メモリ、ディスク容量)を確認 + +4. **データベース接続問題**: + - MySQLコンテナステータス確認: `docker compose ps mysql` + - MySQLログ確認: `docker compose logs mysql` + - 接続テスト: `docker compose exec mysql mysql -u userfeedback -p` + +### デバッグのヒント + +1. **コンテナログ確認**: + + ```bash + # すべてのコンテナログ + docker compose logs + + # 特定のサービスログ + docker compose logs api + docker compose logs web + docker compose logs mysql + ``` + +2. **サービスステータス確認**: + + ```bash + # APIステータス確認 + curl http://localhost:4000/api/health + + # OpenSearchステータス確認(有効な場合) + curl http://localhost:9200/_cluster/health + ``` + +3. **データベース直接アクセス**: + ```bash + # MySQL接続 + docker compose exec mysql mysql -u userfeedback -p userfeedback + ``` + +## 制限事項 + +CLIツールは開発およびテスト環境用に設計されています。本番環境へのデプロイには、次を考慮してください: + +1. **セキュリティ考慮事項**: + + - 機密データには設定ファイルではなく環境変数を使用 + - 適切なシークレット管理を実装 + - 本番レベルのJWTシークレットを使用 + - HTTPS/TLS暗号化を有効化 + +2. **スケーラビリティと可用性**: + + - KubernetesやDocker Swarmなどのオーケストレーションツールを使用 + - ロードバランシングと自動スケーリングを実装 + - 適切なモニタリングとアラートを設定 + - 管理データベースサービス(RDS、Cloud SQLなど)を使用 + +3. **データ管理**: + - 自動化されたバックアップ戦略を実装 + - 適切なバックアップがある永続ボリュームを使用 + - データ保持ポリシーを考慮 + - ディスク使用量とパフォーマンスをモニタリング + +## 次のステップ + +詳細なAPIおよびウェブサーバー設定オプションについては、[環境変数設定](./05-configuration.md)ドキュメントを参照してください。 + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/03-manual-setup.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/03-manual-setup.md new file mode 100644 index 000000000..14c6eb673 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/03-manual-setup.md @@ -0,0 +1,276 @@ +--- +sidebar_position: 3 +title: '手動インストール' +description: 'ソースコードから直接ABC User Feedbackをビルドして実行する手動インストールガイド' +--- + +# 手動インストール + +このドキュメントは、ABC User Feedbackを手動でインストール・構成する方法を説明します。ソースコードから直接アプリケーションをビルドして実行したい場合に便利です。 + +## 前提条件 + +手動インストールを進める前に、次の要件を満たす必要があります: + +- [Node.js v22.19.0以上](https://nodejs.org/en/download/) +- [pnpm v10.15.0以上](https://pnpm.io/installation)(パッケージマネージャー) +- [Git](https://git-scm.com/downloads) +- [MySQL 8.0](https://www.mysql.com/downloads/) +- SMTPサーバー +- (オプション)[OpenSearch 2.16](https://opensearch.org/) + +## ソースコードのダウンロード + +まず、GitHubリポジトリからABC User Feedbackのソースコードをクローンします: + +```bash +git clone https://github.com/line/abc-user-feedback.git +cd abc-user-feedback +``` + +## インフラ設定 + +ABC User FeedbackにはMySQLデータベース、SMTPサーバー、そしてオプションでOpenSearchが必要です。これらのインフラコンポーネントを設定する方法はいくつかあります。 + +### Dockerを使用したインフラ設定 + +最も簡単な方法は、Docker Composeで必要なインフラを設定することです: + +```bash +docker-compose -f docker/docker-compose.infra.yml up -d +``` + +### 既存インフラの使用 + +既にMySQL、OpenSearch、またはSMTPサーバーがある場合は、後で環境変数として接続情報を構成できます。 + +## 依存関係のインストール + +ABC User FeedbackはTurboRepoを通じて管理されるモノレポ構造を使用します。すべてのパッケージの依存関係をインストールするには: + +```bash +pnpm install +``` + +依存関係のインストール後、すべてのパッケージをビルドします: + +```bash +pnpm build +``` + +## 環境変数設定 + +### APIサーバー環境変数 + +`apps/api`ディレクトリに`.env`ファイルを作成し、`.env.example`を参照して構成します: + +```env +# Required environment variables +JWT_SECRET=DEV + +MYSQL_PRIMARY_URL=mysql://userfeedback:userfeedback@localhost:13306/userfeedback # required + +ACCESS_TOKEN_EXPIRED_TIME=10m # default: 10m +REFRESH_TOKEN_EXPIRED_TIME=1h # default: 1h + +# Optional environment variables + +# APP_PORT=4000 # default: 4000 +# APP_ADDRESS=0.0.0.0 # default: 0.0.0.0 + +# MYSQL_SECONDARY_URLS= ["mysql://userfeedback:userfeedback@localhost:13306/userfeedback"] # optional + +SMTP_HOST=localhost # required +SMTP_PORT=25 # required +SMTP_SENDER=user@feedback.com # required +# SMTP_USERNAME= # optional +# SMTP_PASSWORD= # optional +# SMTP_TLS= # default: false +# SMTP_CIPHER_SPEC= # default: TLSv1.2 if SMTP_TLS=true +# SMTP_OPPORTUNISTIC_TLS= # default: true if SMTP_TLS=true + +# OPENSEARCH_USE=false # default: false +# OPENSEARCH_NODE= # required if OPENSEARCH_USE=true +# OPENSEARCH_USERNAME= # optional +# OPENSEARCH_PASSWORD= # optional + +# AUTO_MIGRATION=true # default: true + +# MASTER_API_KEY= # default: none + +# BASE_URL=https://api.example.com # Swaggerドキュメントで使用するAPIサーバーの公開URL(オプション) + +# AUTO_FEEDBACK_DELETION_ENABLED=false # default: false +# AUTO_FEEDBACK_DELETION_PERIOD_DAYS=365*5 +``` + +### ウェブサーバー環境変数 + +`apps/web`ディレクトリに`.env`ファイルを作成し、`.env.example`を参照して構成します: + +```env +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +``` + +環境変数の詳細については、[環境変数設定](./05-configuration.md)ドキュメントを参照してください。 + +## データベースマイグレーション + +APIサーバーを初めて実行する前に、データベーススキーマを作成する必要があります。`AUTO_MIGRATION=true`環境変数を設定すると、サーバー起動時にマイグレーションが自動的に実行されます。 + +手動でマイグレーションを実行するには: + +```bash +cd apps/api +npm run migration:run +``` + +## 開発モードでの実行 + +### 単一コマンドで実行 + +APIサーバーとウェブサーバーを開発モードで実行するには: + +```bash +# プロジェクトルートディレクトリから +pnpm dev +``` + +このコマンドはAPIサーバーとウェブサーバーを同時に起動します。APIサーバーはデフォルトでポート4000で、ウェブサーバーはポート3000で実行されます。 + +### 個別パッケージの実行 + +#### 共通パッケージのビルド + +ウェブアプリケーションを実行する前に、共有パッケージをビルドする必要があります: + +```bash +# プロジェクトルートディレクトリから +cd packages/ufb-shared +pnpm build +``` + +#### UIパッケージのビルド + +ウェブアプリケーションを実行する前に、UIパッケージをビルドする必要があります: + +```bash +# プロジェクトルートディレクトリから +cd packages/ufb-tailwindcss +pnpm build +``` + +#### 各サーバーの個別実行 + +各サーバーを個別に実行するには: + +```bash +# APIサーバーのみ実行 +cd apps/api +pnpm dev + +# ウェブサーバーのみ実行 +cd apps/web +pnpm dev +``` + +## 本番ビルド + +本番環境用のアプリケーションをビルドするには: + +```bash +# プロジェクトルートディレクトリから +pnpm build +``` + +このコマンドはAPIサーバーとウェブサーバーの両方をビルドします。 + +## 本番モードでの実行 + +本番ビルドを実行するには: + +```bash +# APIサーバー実行 +cd apps/api +pnpm start + +# ウェブサーバー実行 +cd apps/web +pnpm start +``` + +## APIタイプ生成 + +バックエンドAPIが実行中の場合、フロントエンド用のAPIタイプを生成できます: + +```bash +cd apps/web +pnpm generate-api-type +``` + +このコマンドはOpenAPI仕様からTypeScriptタイプを生成し、`src/shared/types/api.type.ts`ファイルに保存します。 + +**注意**: このコマンドが正しく動作するには、APIサーバーが`http://localhost:4000`で実行中である必要があります。 + +## コード品質管理 + +### リンティング + +コードリンティングを実行するには: + +```bash +pnpm lint +``` + +### フォーマット + +コードフォーマットを実行するには: + +```bash +pnpm format +``` + +### テスト + +テストを実行するには: + +```bash +pnpm test +``` + +## Swaggerドキュメント + +APIサーバーが実行中の場合、次のエンドポイントでSwaggerドキュメントを確認できます: + +- **APIドキュメント**: http://localhost:4000/docs +- **管理者APIドキュメント**: http://localhost:4000/admin-docs +- **OpenAPI JSON**: http://localhost:4000/docs-json +- **管理者OpenAPI JSON**: http://localhost:4000/admin-docs-json + +> **注意**: APIサーバーをリバースプロキシの後ろで異なるURLで提供する場合、`BASE_URL`環境変数を設定すると、Swaggerドキュメントで正しいAPIエンドポイントURLが生成されます。例: `BASE_URL=https://api.example.com` + +## トラブルシューティング + +### 一般的な問題 + +1. **依存関係インストールエラー**: + - Node.jsバージョンがv22.19.0以上であることを確認してください。 + - pnpmバージョンがv10.15.0以上であることを確認してください。 + - pnpmを最新バージョンに更新してください。 + - `pnpm install --force`を試してください。 + +2. **データベース接続エラー**: + - MySQLサーバーが実行中であることを確認してください。 + - データベース認証情報が正しいことを確認してください。 + - `MYSQL_PRIMARY_URL`環境変数の形式が正しいことを確認してください。 + - Dockerインフラを使用する場合、MySQLがポート13306(3306ではない)で実行されていることを確認してください。 + +3. **ビルドエラー**: + - UIパッケージがビルドされていることを確認してください(`pnpm build:ui`)。 + - すべての依存関係がインストールされていることを確認してください。 + - TypeScriptエラーを確認してください。 + +4. **ランタイムエラー**: + - 環境変数が正しく設定されていることを確認してください。 + - 必要なポートが利用可能であることを確認してください。 + - ログのエラーメッセージを確認してください。 diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/04-smtp-configuration.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/04-smtp-configuration.md new file mode 100644 index 000000000..0d4527850 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/04-smtp-configuration.md @@ -0,0 +1,165 @@ +--- +id: smtp-configuration +title: SMTPサーバー統合ガイド +description: 本番環境で認証メール送信のための外部SMTPサーバー統合方法を案内します。 +sidebar_position: 4 +--- + +# SMTPサーバー統合ガイド + +本番環境では、`smtp4dev`のようなローカルテストサーバーではなく、 +**外部SMTPサーバー(Gmail、SendGrid、会社SMTPなど)**と接続して +認証メール(登録、パスワードリセットなど)を正常に送信できる必要があります。 + +このドキュメントでは、SMTPサーバー統合のための環境変数設定と主要な統合事例を案内します。 + +--- + +## 1. SMTP関連環境変数 + +`api`サービスまたは`.env`ファイルに次の環境変数を設定してください: + +> **参考**: 認証が不要なSMTPサーバーの場合、`SMTP_USERNAME`と`SMTP_PASSWORD`は省略できます。 + +| 環境変数 | 説明 | 必須 | +| ------------------------ | ----------------------------------------------- | --------- | +| `SMTP_HOST` | SMTPサーバーアドレス(例:smtp.gmail.com) | 必須 | +| `SMTP_PORT` | ポート番号(通常587、465など) | 必須 | +| `SMTP_SENDER` | 送信者メールアドレス(例:`noreply@yourdomain.com`) | 必須 | +| `SMTP_USERNAME` | SMTP認証ユーザー名(アカウントID) | オプション | +| `SMTP_PASSWORD` | SMTP認証パスワードまたはAPIキー | オプション | +| `SMTP_TLS` | TLS使用有無(`true`または`false`) | オプション | +| `SMTP_CIPHER_SPEC` | TLS暗号化アルゴリズム(デフォルト:`TLSv1.2`) | オプション | +| `SMTP_OPPORTUNISTIC_TLS` | STARTTLS使用有無(`true`または`false`) | オプション | + +> **重要**: 実際のコードでは`SMTP_USERNAME`と`SMTP_PASSWORD`が使用され、`SMTP_TLS=true`はポート465に、`false`はポート587に主に使用されます。 + +--- + +## 2. Docker環境例 + +```yaml +api: + image: line/abc-user-feedback-api + environment: + - SMTP_HOST=smtp.gmail.com + - SMTP_PORT=587 + - SMTP_USERNAME=your-email@gmail.com + - SMTP_PASSWORD=your-email-app-password + - SMTP_SENDER=noreply@yourdomain.com + - SMTP_TLS=false + - SMTP_OPPORTUNISTIC_TLS=true +``` + +または`.env`ファイルで分離管理できます: + +```env +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME=your-email@gmail.com +SMTP_PASSWORD=your-email-app-password +SMTP_SENDER=noreply@yourdomain.com +SMTP_TLS=false +SMTP_OPPORTUNISTIC_TLS=true +``` + +--- + +## 3. SMTP統合例 + +### ✅ Gmail SMTP統合(個人テスト用) + +- `SMTP_HOST`: `smtp.gmail.com` +- `SMTP_PORT`: `587` +- `SMTP_USERNAME`: Gmailアドレス(例:`abc@gmail.com`) +- `SMTP_PASSWORD`: **アプリパスワード**(安全性の低いアプリを許可 → 非推奨) +- `SMTP_TLS`: `false` +- `SMTP_OPPORTUNISTIC_TLS`: `true` + +> Gmailアカウントに**2段階認証**が有効になっている場合、[アプリパスワード](https://myaccount.google.com/apppasswords)を作成する必要があります。 + +--- + +### ✅ SendGrid統合(推奨) + +- `SMTP_HOST`: `smtp.sendgrid.net` +- `SMTP_PORT`: `587` +- `SMTP_USERNAME`: `apikey` +- `SMTP_PASSWORD`: 実際のSendGrid APIキー +- `SMTP_SENDER`: 確認済み送信者アドレス +- `SMTP_TLS`: `false` +- `SMTP_OPPORTUNISTIC_TLS`: `true` + +--- + +## 4. テスト方法 + +### 4.1 メール送信テスト + +1. **メール認証テスト**: + + - 管理者またはユーザーアカウント作成 + - メール認証コード送信確認 + +2. **パスワードリセットテスト**: + + - パスワードリセットリクエスト + - リセットリンクが含まれたメール受信確認 + +3. **ユーザー招待テスト**: + - 管理者が新しいユーザーを招待 + - 招待メール送信確認 + +### 4.2 ログ確認 + +メール送信失敗時、次のコマンドで詳細ログを確認してください: + +```bash +# Docker Compose環境 +docker compose logs api + +# 特定時間帯のログ確認 +docker compose logs --since=10m api + +# リアルタイムログモニタリング +docker compose logs -f api +``` + +SMTPエラーが発生すると、ログに詳細メッセージが表示されます。 + +--- + +## 5. トラブルシューティング + +| 問題タイプ | 原因または解決方法 | +| -------------------------- | ---------------------------------------- | +| 認証エラー(`535`) | `SMTP_USERNAME` / `SMTP_PASSWORD`再確認 | +| 接続拒否(`ECONNREFUSED`) | ファイアウォールまたは誤ったポート設定 | +| メールが届かない | `SMTP_SENDER`が認証されていない | +| TLSエラー(`ETLS`) | `SMTP_TLS`設定が誤っている | +| STARTTLS失敗 | `SMTP_OPPORTUNISTIC_TLS`設定確認 | + +--- + +## 6. SMTPに関連するメールテンプレート + +現在、システムでメールは次の状況で送信されます: + +- **メール認証**: 管理者/ユーザー登録時に認証コード送信 +- **パスワードリセット**: パスワードリセットリクエスト時にリンク送信 +- **ユーザー招待**: 管理者がユーザーを招待するときに招待メール送信 + +メール内容は**Handlebarsテンプレート**ベースで構成されており、次の情報が含まれます: + +- 送信者: `"User feedback" ` +- 基本URL: `ADMIN_WEB_URL`環境変数値を使用 +- テンプレート位置: `src/configs/modules/mailer-config/templates/` + +--- + +## 関連ドキュメント + +- [Docker Hubインストールガイド](./docker-hub-images) +- [環境変数設定](./configuration) +- [初期設定ガイド](/ja/user-guide/getting-started) + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/05-configuration.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/05-configuration.md new file mode 100644 index 000000000..0c4e2cad6 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/05-configuration.md @@ -0,0 +1,159 @@ +--- +id: configuration +title: 環境変数構成 +description: ABC User FeedbackのAPIおよびウェブサーバーの環境変数構成方法を説明します。 +sidebar_position: 5 +--- + +# 環境変数構成 + +このドキュメントでは、ABC User Feedbackの**APIサーバー**および**ウェブサーバー**で使用する主要な環境変数と設定方法を説明します。 + +--- + +## 1. APIサーバー環境変数 + +### 必須環境変数 + +| 環境変数 | 説明 | デフォルト | 例 | +| ---------------------------- | ------------------------- | ---------- | -------------------------------- | +| `JWT_SECRET` | JWT署名用シークレットキー | なし | `jwtsecretjwtsecretjwtsecret` | +| `MYSQL_PRIMARY_URL` | MySQL接続URL | なし | `mysql://user:pass@host:3306/db` | +| `ACCESS_TOKEN_EXPIRED_TIME` | Access Token有効期間 | `10m` | `10m`、`30s`、`1h` | +| `REFRESH_TOKEN_EXPIRED_TIME` | Refresh Token有効期間 | `1h` | `1h`、`7d` | + +> JWTシークレットは十分に複雑で安全な文字列を使用する必要があります。 + +⚠️ **セキュリティ注意事項**: + +- `JWT_SECRET`は最低32文字以上の複雑な文字列を使用してください +- 本番環境では絶対にデフォルト値を使用しないでください +- 環境変数ファイル(`.env`)はバージョン管理に含めないでください +- 機密情報は環境変数やシークレット管理システムを通じて管理してください + +--- + +### オプション環境変数 + +| 環境変数 | 説明 | デフォルト | 例 | +| ---------------------- | ------------------------------------------------- | ----------------------- | --------------------------- | +| `APP_PORT` | APIサーバーポート | `4000` | `4000` | +| `APP_ADDRESS` | バインドアドレス | `0.0.0.0` | `127.0.0.1` | +| `ADMIN_WEB_URL` | 管理者ウェブURL | `http://localhost:3000` | `https://admin.company.com` | +| `BASE_URL` | Swaggerドキュメントで使用するAPIサーバーの公開URL | なし | `https://api.example.com` | +| `MYSQL_SECONDARY_URLS` | セカンダリDB URL(JSON配列) | なし | `["mysql://..."]` | +| `AUTO_MIGRATION` | アプリ起動時のDB自動マイグレーション | `true` | `false` | +| `MASTER_API_KEY` | マスター権限APIキー(オプション) | なし | `abc123xyz` | +| `NODE_OPTIONS` | Node実行オプション | なし | `--max_old_space_size=4096` | + +--- + +### SMTP設定(メール認証) + +| 環境変数 | 説明 | 例 | +| ------------------------ | -------------------------------- | ------------------------------ | +| `SMTP_HOST` | SMTPサーバーアドレス | `smtp.gmail.com` | +| `SMTP_PORT` | ポート(通常587または465) | `587` | +| `SMTP_USERNAME` | ログインユーザー | `user@example.com` | +| `SMTP_PASSWORD` | ログインパスワードまたはトークン | `app-password` | +| `SMTP_SENDER` | 送信者アドレス | `noreply@company.com` | +| `SMTP_BASE_URL` | メール内リンク用基本URL | `https://feedback.company.com` | +| `SMTP_TLS` | TLS使用有無 | `true` | +| `SMTP_CIPHER_SPEC` | 暗号化仕様 | `TLSv1.2` | +| `SMTP_OPPORTUNISTIC_TLS` | STARTTLSサポート有無 | `true` | + +📎 詳細設定については、[SMTP統合ガイド](./04-smtp-configuration.md)を参照してください。 + +--- + +## 2. OpenSearch設定(オプション) + +| 環境変数 | 説明 | 例 | +| --------------------- | -------------------- | ----------------------- | +| `OPENSEARCH_USE` | OpenSearch有効化有無 | `true` | +| `OPENSEARCH_NODE` | OpenSearchノードURL | `http://localhost:9200` | +| `OPENSEARCH_USERNAME` | 認証ID | `admin` | +| `OPENSEARCH_PASSWORD` | 認証パスワード | `admin123` | + +> OpenSearchは検索速度向上およびAI機能改善に使用されます。 + +--- + +## 3. 自動フィードバック削除設定 + +| 環境変数 | 説明 | デフォルト / 条件 | +| ------------------------------------ | -------------------------------- | ----------------------- | +| `AUTO_FEEDBACK_DELETION_ENABLED` | 古いフィードバック削除機能有効化 | `false` | +| `AUTO_FEEDBACK_DELETION_PERIOD_DAYS` | 削除基準日数 | `365`(有効な場合必須) | + +--- + +## 4. ウェブサーバー環境変数 + +### 必須環境変数 + +| 環境変数 | 説明 | 例 | +| -------------------------- | ----------------------------------------- | ----------------------- | +| `NEXT_PUBLIC_API_BASE_URL` | クライアントで使用するAPIサーバーアドレス | `http://localhost:4000` | + +### オプション環境変数 + +| 環境変数 | 説明 | デフォルト | 例 | +| -------- | -------------------- | ---------- | ------ | +| `PORT` | フロントエンドポート | `3000` | `3000` | + +--- + +## 5. 設定方法 + +### Docker Compose例 + +```yaml +services: + api: + image: line/abc-user-feedback-api + environment: + - JWT_SECRET=changeme + - MYSQL_PRIMARY_URL=mysql://user:pass@mysql:3306/userfeedback + - SMTP_HOST=smtp.sendgrid.net + - SMTP_USERNAME=apikey + - SMTP_PASSWORD=your-sendgrid-key +``` + +### .envファイル例 + +``` +# apps/api/.env +JWT_SECRET=changemechangemechangeme +MYSQL_PRIMARY_URL=mysql://root:pass@localhost:3306/db +ACCESS_TOKEN_EXPIRED_TIME=10m +REFRESH_TOKEN_EXPIRED_TIME=1h +SMTP_HOST=smtp.example.com +SMTP_SENDER=noreply@example.com +# BASE_URL=https://api.example.com # リバースプロキシの後ろで提供する場合に設定 + +# apps/web/.env +NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 +``` + +--- + +## 7. トラブルシューティングガイド + +| 問題 | 原因と解決策 | +| ---------------------- | ----------------------------------------- | +| 環境変数が認識されない | `.env`位置確認またはコンテナ再起動 | +| DB接続失敗 | `MYSQL_PRIMARY_URL`形式または接続情報確認 | +| SMTPエラー | ポート/TLS設定または認証情報再確認 | +| OpenSearchエラー | ノードURLまたはユーザー認証確認 | +| JWTトークンエラー | `JWT_SECRET`長さおよび複雑性確認 | +| 環境変数検証失敗 | 必須環境変数欠落またはタイプエラー確認 | +| ポート競合 | `APP_PORT`、`PORT`設定確認 | + +--- + +## 関連ドキュメント + +- [Dockerインストールガイド](./docker-hub-images) +- [SMTP統合ガイド](./smtp-configuration) +- [初期設定ガイド](/ja/user-guide/getting-started) diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/_category_.json b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/_category_.json new file mode 100644 index 000000000..501dc4715 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 1, + "label": "インストール", + "description": "開発環境の設定とインストールガイドです。" +} diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/index.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/index.md new file mode 100644 index 000000000..67250ad49 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/01-installation/index.md @@ -0,0 +1,7 @@ +--- +title: インストール +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/02-api-integration.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/02-api-integration.md new file mode 100644 index 000000000..1d6c1f85f --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/02-api-integration.md @@ -0,0 +1,533 @@ +--- +sidebar_position: 2 +title: "API統合" +description: "ABC User Feedback APIを活用した外部システム統合方法と実際の実装例を案内します。" +--- + +# API統合 + +ABC User Feedbackは**RESTful API**を通じて外部システムと統合できます。プログラムでフィードバックを収集し、イシューを管理し、データを照会できるため、既存のサービスやワークフローに簡単に統合できます。 + +--- + +## API基本情報 + +### 公式APIドキュメント + +ABC User Feedbackの**完全なAPIドキュメント**は次のリンクで確認できます: + +🔗 **[公式APIドキュメント(Redocly)](https://line.github.io/abc-user-feedback/)** + +このドキュメントでは、すべてのエンドポイントの詳細な仕様、リクエスト/レスポンス例、実際にテスト可能なインターフェースを提供します。 + +### Base URL + +``` +https://your-domain.com/api +``` + +### 認証方式 + +すべてのAPIリクエストは**APIキーベースの認証**を使用します。 + +```http +X-API-KEY: your-api-key-here +Content-Type: application/json +``` + +:::warning セキュリティ注意事項 +APIキーはサーバーサイドでのみ使用し、クライアント(ブラウザ、モバイルアプリ)に公開しないでください。 +::: + +### APIキー発行方法 + +1. **管理者ページアクセス**: ABC User Feedback管理者ページにログイン +2. **プロジェクト設定**: 該当プロジェクトの設定ページに移動 +3. **APIキー管理**: 「APIキー管理」メニューから新しいAPIキーを生成 +4. **キーコピー**: 生成されたAPIキーを安全な場所に保存 + +:::info APIキー権限 +APIキーはプロジェクトごとに発行され、該当プロジェクトのデータにのみアクセスできます。 +::: + +--- + +## 主要APIエンドポイント例 + +### 1. フィードバック作成 + +#### 基本フィードバック作成 + +```javascript +const createFeedback = async ( + projectId, + channelId, + message, + issueNames = [] +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + message: message, + issueNames: issueNames, + }), + } + ); + + return await response.json(); +}; + +// 使用例 +const feedback = await createFeedback(1, 1, "決済エラーが発生しました", [ + "決済", + "エラー", +]); +``` + +### 2. フィードバック照会 + +#### チャネル別フィードバック検索 + +```javascript +const searchFeedbacks = async ( + projectId, + channelId, + searchText, + limit = 10, + page = 1 +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/search`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + limit: limit, + page: page, + query: { + searchText: searchText, + createdAt: { + gte: "2024-01-01", + lt: "2024-12-31", + }, + }, + sort: { + createdAt: "DESC", + }, + }), + } + ); + + return await response.json(); +}; + +// 使用例 +const feedbacks = await searchFeedbacks(1, 1, "決済", 20, 1); +console.log( + `合計${feedbacks.meta.totalItems}件のフィードバック中${feedbacks.items.length}件を照会` +); +``` + +#### 単一フィードバック照会 + +```javascript +const getFeedbackById = async (projectId, channelId, feedbackId) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, + { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 使用例 +const feedback = await getFeedbackById(1, 1, 123); +console.log("フィードバック詳細:", feedback); +``` + +#### フィードバック更新 + +```javascript +const updateFeedback = async (projectId, channelId, feedbackId, updateData) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify(updateData), + } + ); + + return await response.json(); +}; + +// 使用例 +const updatedFeedback = await updateFeedback(1, 1, 123, { + message: "更新されたフィードバック内容", + issueNames: ["更新されたイシュー"], +}); +``` + +#### フィードバック削除 + +```javascript +const deleteFeedbacks = async (projectId, channelId, feedbackIds) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + feedbackIds: feedbackIds, + }), + } + ); + + return await response.json(); +}; + +// 使用例 +const result = await deleteFeedbacks(1, 1, [123, 124, 125]); +console.log("削除完了:", result); +``` + +### 3. イシュー管理 + +#### イシュー作成 + +```javascript +const createIssue = async (projectId, name, description) => { + const response = await fetch(`/api/projects/${projectId}/issues`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + name: name, + description: description, + }), + }); + + return await response.json(); +}; + +// 使用例 +const issue = await createIssue( + 1, + "決済エラー", + "ユーザーが決済過程でエラーを経験" +); +``` + +#### イシュー検索 + +```javascript +const searchIssues = async (projectId, query = {}) => { + const response = await fetch(`/api/projects/${projectId}/issues/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + limit: 10, + page: 1, + query: query, + sort: { + createdAt: "DESC", + }, + }), + }); + + return await response.json(); +}; + +// 使用例 +const issues = await searchIssues(1, { name: "決済" }); +``` + +#### イシュー照会 + +```javascript +const getIssueById = async (projectId, issueId) => { + const response = await fetch(`/api/projects/${projectId}/issues/${issueId}`, { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + }); + + return await response.json(); +}; + +// 使用例 +const issue = await getIssueById(1, 123); +console.log("イシュー詳細:", issue); +``` + +#### イシュー更新 + +```javascript +const updateIssue = async (projectId, issueId, updateData) => { + const response = await fetch(`/api/projects/${projectId}/issues/${issueId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify(updateData), + }); + + return await response.json(); +}; + +// 使用例 +const updatedIssue = await updateIssue(1, 123, { + name: "更新されたイシュー名", + description: "更新されたイシュー説明", +}); +``` + +#### イシュー削除 + +```javascript +const deleteIssues = async (projectId, issueIds) => { + const response = await fetch(`/api/projects/${projectId}/issues`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "your-api-key-here", + }, + body: JSON.stringify({ + issueIds: issueIds, + }), + }); + + return await response.json(); +}; + +// 使用例 +const result = await deleteIssues(1, [123, 124, 125]); +console.log("イシュー削除完了:", result); +``` + +#### フィードバックにイシュー追加 + +```javascript +const addIssueToFeedback = async ( + projectId, + channelId, + feedbackId, + issueId +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}/issues/${issueId}`, + { + method: "POST", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 使用例 +const result = await addIssueToFeedback(1, 1, 123, 456); +console.log("イシュー追加完了:", result); +``` + +#### フィードバックからイシュー削除 + +```javascript +const removeIssueFromFeedback = async ( + projectId, + channelId, + feedbackId, + issueId +) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/feedbacks/${feedbackId}/issues/${issueId}`, + { + method: "DELETE", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 使用例 +const result = await removeIssueFromFeedback(1, 1, 123, 456); +console.log("イシュー削除完了:", result); +``` + +### 4. プロジェクトとチャネル情報 + +#### プロジェクト情報照会 + +```javascript +const getProjectInfo = async (projectId) => { + const response = await fetch(`/api/projects/${projectId}`, { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + }); + + return await response.json(); +}; + +// 使用例 +const project = await getProjectInfo(1); +console.log("プロジェクト情報:", project); +``` + +#### チャネルフィールド照会 + +```javascript +const getChannelFields = async (projectId, channelId) => { + const response = await fetch( + `/api/projects/${projectId}/channels/${channelId}/fields`, + { + method: "GET", + headers: { + "X-API-KEY": "your-api-key-here", + }, + } + ); + + return await response.json(); +}; + +// 使用例 +const fields = await getChannelFields(1, 1); +console.log("チャネルフィールド:", fields); +``` + +--- + +## SwaggerによるAPIテスト + +ABC User Feedbackは**Swagger UI**を提供して、APIを簡単にテストし理解できます。 + +### Swaggerアクセス方法 + +**APIサーバーアドレス + `/docs`**でアクセスします: + +``` +https://your-domain.com/api/docs +``` + +または**ReDoc形式**で: + +``` +https://your-domain.com/api/docs/redoc +``` + +### SwaggerでのAPIキー設定 + +1. Swagger UI上部の**"Authorize"**ボタンをクリック +2. **X-API-KEY**フィールドに発行されたAPIキーを入力 +3. **"Authorize"**をクリックして認証完了 + +これ以降、すべてのAPIリクエストで自動的にAPIキーが含まれ、テストできます。 + +### Swagger活用のヒント + +- **"Try it out"**ボタンで実際のAPI呼び出しテスト +- **Response body**セクションで実際のレスポンスデータ構造を確認 +- **Schema**タブでリクエスト/レスポンスデータ形式の詳細を確認 +- **cURL**コマンドを自動生成してCLIテスト可能 + +--- + +## エラー処理と再試行ロジック + +### HTTPステータスコード + +| ステータスコード | 意味 | 処理方法 | +| --------- | -------------- | ----------------------- | +| **200** | 成功 | 正常処理 | +| **400** | 不正なリクエスト | リクエストデータ検証 | +| **401** | 認証失敗 | APIキー確認 | +| **403** | 権限なし | プロジェクトアクセス権限確認 | +| **404** | リソースなし | ID値確認 | +| **429** | リクエスト制限超過 | しばらくしてから再試行 | +| **500** | サーバーエラー | 再試行またはサポートチームに問い合わせ | + +## レスポンスデータ解析方法 + +### ページネーションレスポンス構造 + +```json +{ + "meta": { + "itemCount": 10, + "totalItems": 100, + "itemsPerPage": 10, + "totalPages": 10, + "currentPage": 1 + }, + "items": [ + { + "id": 1, + "message": "フィードバック内容", + "createdAt": "2024-01-01T00:00:00.000Z", + "issues": [ + { + "id": 1, + "name": "イシュー名" + } + ] + } + ] +} +``` + +## セキュリティとパフォーマンス最適化 + +### APIキーセキュリティ + +- **環境変数使用**: APIキーを環境変数で管理 +- **サーバーサイドのみ**: クライアントにAPIキーを公開しない +- **キーローテーション**: 定期的なAPIキー交換 +- **IPホワイトリスト**: 可能な場合は特定IPからのみアクセス許可 + +### パフォーマンス最適化 + +- **ページネーション活用**: 大量データ照会時に適切なlimit設定 +- **必要なフィールドのみリクエスト**: クエリ最適化でレスポンス速度改善 +- **キャッシング戦略**: 頻繁に照会するデータはクライアントサイドキャッシング +- **バッチ処理**: 複数のリクエストをまとめて処理 + +## 関連ドキュメント + +- [APIキー管理](/ja/user-guide/settings/api-key-management) - UIからAPIキーを発行する方法 +- [画像設定](/ja/user-guide/settings/image-setting) - 画像アップロードAPI使用のための設定 +- [Webhook統合](/ja/user-guide/settings/webhook-management) - APIと一緒に活用できるリアルタイム通知設定 + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/03-oauth-integration.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/03-oauth-integration.md new file mode 100644 index 000000000..31a6b8fb1 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/03-oauth-integration.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 3 +title: "OAuth統合" +description: "Google OAuthおよびカスタムOAuthプロバイダーによるシングルサインオン(SSO)統合方法を案内します。" +--- + +# OAuth統合 + +ABC User FeedbackでOAuth 2.0ベースのシングルサインオン(SSO)を設定すると、ユーザーは別のアカウント作成なしで既存のアカウント(Google、Microsoft、GitHubなど)でログインできます。これはユーザーの利便性を向上させ、企業環境で統合認証を実装するために不可欠です。 + +--- + +## OAuth統合の概要 + +ABC User FeedbackでサポートされるOAuth方式: + +### 1. Google OAuth + +- 追加設定なしでデフォルト提供 +- Googleアカウントによる簡単なログイン + +### 2. カスタムOAuthプロバイダー + +- 社内認証システム +- その他のOAuth 2.0/OpenID Connect互換サービス + +OAuthを設定すると、既存のメールログインと並行して使用でき、組織ポリシーに応じてOAuthのみを許可するように制限することもできます。 + +--- + +## Google OAuth統合設定 + +### Google Cloud Consoleでの設定 + +#### 1. Google Cloud Consoleにアクセス + +[Google Cloud Console](https://console.cloud.google.com)にアクセスしてプロジェクトを作成するか、既存のプロジェクトを選択します。 + +#### 2. OAuth 2.0クライアントID作成 + +1. **APIとサービス > 認証情報**メニューに移動 +2. **+ 認証情報を作成 > OAuthクライアントID**を選択 +3. アプリケーションタイプを**ウェブアプリケーション**として選択 + +#### 3. 承認済みリダイレクトURI設定 + +**承認済みリダイレクトURI**に次のURLを追加します: + +``` +https://your-domain.com/auth/oauth-callback +``` + +例: + +- `https://feedback.company.com/auth/oauth-callback` +- `http://localhost:3000/auth/oauth-callback`(開発環境) + +#### 4. クライアント情報確認 + +作成完了後、次の情報を確認してコピーしておきます: + +- **クライアントID**: `1234567890-abc123def456.apps.googleusercontent.com` +- **クライアントシークレット**: `GOCSPX-abcdef123456` + +### ABC User FeedbackでのGoogle OAuth設定 + +Google OAuthを使用するには、次の手順に従って設定する必要があります: + +#### 1. Google OAuth設定有効化 + +**Settings > Login Management**で: + +1. **OAuth2.0 Login**トグルを有効化 +2. **Login Button Type**を"Google Login"として選択 +3. Google Cloud Consoleで取得した情報を入力: + - **Client ID**: Google Cloud Consoleで作成したクライアントID + - **Client Secret**: Google Cloud Consoleで作成したクライアントシークレット + - **Authorization Code Request URL**: `https://accounts.google.com/o/oauth2/v2/auth` + - **Scope**: `openid email profile` + - **Access Token URL**: `https://oauth2.googleapis.com/token` + - **User Profile Request URL**: `https://www.googleapis.com/oauth2/v2/userinfo` + - **Email Key**: `email` + +#### 2. リダイレクトURI登録 + +Google Cloud Consoleで次のURLを**承認済みリダイレクトURI**に追加: + +``` +https://your-domain.com/auth/oauth-callback +``` + +開発環境の場合: + +``` +http://localhost:3000/auth/oauth-callback +``` + +--- + +## カスタムOAuthプロバイダー統合 + +### 社内認証システム統合 + +ABC User Feedbackは、企業環境で使用される社内認証システムと統合できます。ほとんどの社内認証システムはOAuth 2.0またはOpenID Connect標準をサポートしているため、標準OAuthフローを通じて統合が可能です。 + +#### 社内認証システム設定要件 + +社内認証システムと統合するには、次の情報が必要です: + +1. **OAuthクライアント登録** + + - クライアントID + - クライアントシークレット + - リダイレクトURI: `https://your-domain.com/auth/oauth-callback` + +2. **OAuthエンドポイント情報** + + - Authorization URL(認証リクエストURL) + - Token URL(トークン交換URL) + - User Info URL(ユーザー情報照会URL) + +3. **権限範囲(Scope)** + - ユーザープロフィール情報アクセス権限 + - メールアドレスアクセス権限 + +#### 一般的な社内認証システム例 + +| 項目 | 説明 | 社内システム例 | +| ---------------------------------- | ---------------------------------- | ------------------------------------------ | +| **Login Button Type** | ログインボタンタイプ | `CUSTOM` | +| **Login Button Name** | ログインボタンに表示される名前 | `社内アカウントでログイン` | +| **Client ID** | OAuthクライアントID | `company-auth-client-123` | +| **Client Secret** | クライアントシークレット | `company-secret-abc123` | +| **Authorization Code Request URL** | ユーザー認証リクエストURL | `https://auth.company.com/oauth/authorize` | +| **Scope** | リクエストする権限範囲 | `openid email profile` | +| **Access Token URL** | トークンリクエストURL | `https://auth.company.com/oauth/token` | +| **User Profile Request URL** | ユーザー情報照会API | `https://auth.company.com/api/user` | +| **Email Key** | ユーザー情報JSONのメールフィールド名 | `email`または`mail` | + +### その他のOAuth 2.0/OpenID Connect互換サービス + +ABC User Feedbackは、OAuth 2.0またはOpenID Connect標準に準拠するすべての認証サービスと統合できます。 + +#### サポート可能なサービスタイプ + +- **OpenID Connectプロバイダー**: 標準OpenID Connectプロトコルをサポートするサービス +- **OAuth 2.0プロバイダー**: OAuth 2.0 Authorization Codeフローをサポートするサービス +- **カスタム認証サーバー**: 標準OAuthエンドポイントを提供する自己構築サービス + +#### 統合設定方法 + +**Settings > Login Management**でカスタムOAuthを設定します: + +1. **管理者アカウントでログイン**後、**Settings > Login Management**メニューに移動 +2. **OAuth2.0 Login**トグルを有効化 +3. **Login Button Type**を`CUSTOM`として選択 +4. 認証サービスプロバイダーから受け取った情報を入力: + - **Login Button Name**: ログインボタンに表示されるテキスト(例:「社内アカウントでログイン」) + - **Client ID**: OAuthクライアント識別子 + - **Client Secret**: クライアント認証シークレット + - **Authorization Code Request URL**: ユーザー認証リクエストURL + - **Scope**: リクエストする権限範囲(スペース区切り、例:「openid email profile」) + - **Access Token URL**: アクセストークンリクエストURL + - **User Profile Request URL**: ユーザープロフィール情報照会URL + - **Email Key**: ユーザー情報JSONのメールフィールド名(例:「email」または「mail」) + +--- + +## 関連ドキュメント + +- [ログイン管理](/ja/user-guide/settings/tenant-settings) - UIでOAuthを設定する方法 + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/04-webhook-integration.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/04-webhook-integration.md new file mode 100644 index 000000000..26f9e5ee1 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/04-webhook-integration.md @@ -0,0 +1,303 @@ +--- +sidebar_position: 4 +title: 'ウェブフック統合' +description: 'ウェブフックを活用して外部システムとリアルタイム統合する方法と実装例を案内します。' +--- + +# ウェブフック統合 + +ウェブフックにより、ABC User Feedbackで発生する主要イベントをリアルタイムで外部システムに配信できます。Slack通知、自動化ワークフロー、カスタム分析システムなどと統合できます。 + +--- + +## サポートされるイベントタイプ + +ABC User Feedbackでサポートされるイベントは次のとおりです: + +### 1. FEEDBACK_CREATION + +新しいフィードバックが作成されたときに発生します。 + +**リクエストヘッダー:** + +``` +Content-Type: application/json +x-webhook-token: your-secret-token +``` + +**ペイロード例:** + +```json +{ + "event": "FEEDBACK_CREATION", + "data": { + "feedback": { + "id": 123, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "message": "ユーザーフィードバック内容", + "userEmail": "user@example.com", + "issues": [ + { + "id": 456, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "name": "バグレポート", + "description": "イシュー説明", + "status": "OPEN", + "externalIssueId": "EXT-123", + "feedbackCount": 5 + } + ] + }, + "channel": { + "id": 1, + "name": "ウェブサイトフィードバック" + }, + "project": { + "id": 1, + "name": "My Project" + } + } +} +``` + +### 2. ISSUE_CREATION + +新しいイシューが作成されたときに発生します。 + +**ペイロード例:** + +```json +{ + "event": "ISSUE_CREATION", + "data": { + "issue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T11:00:00.000Z", + "name": "新しいイシュー", + "description": "イシュー説明", + "status": "OPEN", + "externalIssueId": "EXT-789", + "feedbackCount": 0 + }, + "project": { + "id": 1, + "name": "My Project" + } + } +} +``` + +### 3. ISSUE_STATUS_CHANGE + +イシューステータスが変更されたときに発生します。 + +**ペイロード例:** + +```json +{ + "event": "ISSUE_STATUS_CHANGE", + "data": { + "issue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T12:00:00.000Z", + "name": "イシュー名", + "description": "イシュー説明", + "status": "IN_PROGRESS", + "externalIssueId": "EXT-789", + "feedbackCount": 3 + }, + "project": { + "id": 1, + "name": "My Project" + }, + "previousStatus": "OPEN" + } +} +``` + +### 4. ISSUE_ADDITION + +フィードバックにイシューが追加されたときに発生します。 + +**ペイロード例:** + +```json +{ + "event": "ISSUE_ADDITION", + "data": { + "feedback": { + "id": 123, + "createdAt": "2024-01-15T10:30:00.000Z", + "updatedAt": "2024-01-15T10:30:00.000Z", + "message": "ユーザーフィードバック内容", + "issues": [ + { + "id": 456, + "name": "既存イシュー", + "status": "OPEN" + }, + { + "id": 789, + "name": "新しく追加されたイシュー", + "status": "OPEN" + } + ] + }, + "channel": { + "id": 1, + "name": "ウェブサイトフィードバック" + }, + "project": { + "id": 1, + "name": "My Project" + }, + "addedIssue": { + "id": 789, + "createdAt": "2024-01-15T11:00:00.000Z", + "updatedAt": "2024-01-15T11:00:00.000Z", + "name": "新しく追加されたイシュー", + "description": "イシュー説明", + "status": "OPEN", + "externalIssueId": "EXT-456", + "feedbackCount": 1 + } + } +} +``` + +--- + +## ウェブフック受信サーバー実装 + +ウェブフックを受信するためのHTTPサーバーを実装する必要があります。サーバーは次の要件を満たす必要があります: + +### 基本要件 + +1. **HTTP POSTリクエスト処理**: ウェブフックはHTTP POSTで送信されます +2. **JSONペイロード解析**: リクエスト本文はJSON形式です +3. **200レスポンスコード返却**: 処理成功時は必ず200ステータスコードで応答 + +### 実装例(Node.js/Express) + +```javascript +const express = require('express'); +const app = express(); + +app.use(express.json()); + +app.post('/webhook', (req, res) => { + const { event, data } = req.body; + const token = req.headers['x-webhook-token']; + + // トークン検証 + if (token !== 'your-secret-token') { + return res.status(401).json({ error: 'Unauthorized' }); + } + + // イベント処理 + switch (event) { + case 'FEEDBACK_CREATION': + console.log('新しいフィードバック作成:', data.feedback); + // フィードバック処理ロジック + break; + case 'ISSUE_CREATION': + console.log('新しいイシュー作成:', data.issue); + // イシュー処理ロジック + break; + case 'ISSUE_STATUS_CHANGE': + console.log( + 'イシューステータス変更:', + data.issue, + '以前のステータス:', + data.previousStatus, + ); + // ステータス変更処理ロジック + break; + case 'ISSUE_ADDITION': + console.log('イシュー追加:', data.addedIssue); + // イシュー追加処理ロジック + break; + } + + res.status(200).json({ success: true }); +}); + +app.listen(3000, () => { + console.log('ウェブフックリスナーサーバーがポート3000で実行中です。'); +}); +``` + +--- + +## セキュリティと再試行ポリシー + +### セキュリティ考慮事項 + +- **トークン検証**: `x-webhook-token`ヘッダーを通じてリクエストを検証します +- **HTTPS使用**: 本番環境では必ずHTTPSを使用してください + +### 再試行ポリシー + +- **自動再試行**: ABC User Feedbackはウェブフック送信失敗時に最大3回まで自動再試行します +- **再試行間隔**: 各再試行は3秒後に実行されます + +### エラー処理 + +- **4xxエラー**: クライアントエラーと見なされ、再試行しません +- **5xxエラー**: サーバーエラーと見なされ、再試行します +- **ネットワークエラー**: 接続失敗時に再試行します + +--- + +## 活用事例 + +### 1. 自動翻訳 + +```javascript +// FEEDBACK_CREATIONイベントを受信して自動翻訳 +if (event === 'FEEDBACK_CREATION') { + const translatedMessage = await translateText(data.feedback.message); + // 翻訳された内容をフィードバックに更新 + await updateFeedback(data.feedback.id, { translatedMessage }); +} +``` + +### 2. 外部チケットシステム統合 + +```javascript +// ISSUE_CREATIONイベントを受信して外部システムにチケット作成 +if (event === 'ISSUE_CREATION') { + const ticketId = await createExternalTicket({ + title: data.issue.name, + description: data.issue.description, + priority: 'medium', + }); + // 外部チケットIDをイシューに保存 + await updateIssue(data.issue.id, { externalIssueId: ticketId }); +} +``` + +### 3. 通知システム統合 + +```javascript +// ISSUE_STATUS_CHANGEイベントを受信してチームに通知 +if (event === 'ISSUE_STATUS_CHANGE') { + await sendSlackNotification({ + channel: '#feedback-alerts', + message: `イシュー"${data.issue.name}"のステータスが${data.previousStatus}から${data.issue.status}に変更されました。`, + }); +} +``` + +--- + +## 関連ドキュメント + +- [ウェブフック管理](/ja/user-guide/settings/webhook-management) - UIでウェブフックを設定する方法 +- [API統合](./02-api-integration.md) - ウェブフックと一緒に使用できるAPI活用 +- [イシュー管理](/ja/user-guide/issue-management) - イシューステータス変更イベントの理解 + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/_category_.json b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/_category_.json new file mode 100644 index 000000000..3f43a63c5 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/_category_.json @@ -0,0 +1,5 @@ +{ + "position": 3, + "label": "開発者ガイド" +} + diff --git a/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/index.md b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/index.md new file mode 100644 index 000000000..c82fa85ed --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-plugin-content-docs/current/02-developer-guide/index.md @@ -0,0 +1,7 @@ +--- +title: 開発者ガイド +--- + +import DocCardList from '@theme/DocCardList'; + + diff --git a/apps/docs/i18n/ja/docusaurus-theme-classic/footer.json b/apps/docs/i18n/ja/docusaurus-theme-classic/footer.json new file mode 100644 index 000000000..1068dcbd5 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-theme-classic/footer.json @@ -0,0 +1,14 @@ +{ + "copyright": { + "message": "Copyright © {year} ABC User Feedback.", + "description": "The footer copyright" + }, + "link.title.Docs": { + "message": "ドキュメント", + "description": "The title of the footer links column with title=Docs in the footer" + }, + "link.title.More": { + "message": "その他", + "description": "The title of the footer links column with title=More in the footer" + } +} diff --git a/apps/docs/i18n/ja/docusaurus-theme-classic/navbar.json b/apps/docs/i18n/ja/docusaurus-theme-classic/navbar.json new file mode 100644 index 000000000..72d6ce1b7 --- /dev/null +++ b/apps/docs/i18n/ja/docusaurus-theme-classic/navbar.json @@ -0,0 +1,18 @@ +{ + "title": { + "message": "ABC User Feedback", + "description": "The title in the navbar" + }, + "item.label.Docs": { + "message": "ドキュメント", + "description": "Navbar item with label Docs" + }, + "item.label.GitHub": { + "message": "GitHub", + "description": "Navbar item with label GitHub" + }, + "logo.alt": { + "message": "ロゴ", + "description": "The alt text of navbar logo" + } +} diff --git a/apps/docs/i18n/ko/code.json b/apps/docs/i18n/ko/code.json new file mode 100644 index 000000000..0630b4c76 --- /dev/null +++ b/apps/docs/i18n/ko/code.json @@ -0,0 +1,313 @@ +{ + "theme.ErrorPageContent.title": { + "message": "페이지에 오류가 발생하였습니다.", + "description": "The title of the fallback page when the page crashed" + }, + "theme.BackToTopButton.buttonAriaLabel": { + "message": "맨 위로 스크롤하기", + "description": "The ARIA label for the back to top button" + }, + "theme.blog.archive.title": { + "message": "게시물 목록", + "description": "The page & hero title of the blog archive page" + }, + "theme.blog.archive.description": { + "message": "게시물 목록", + "description": "The page & hero description of the blog archive page" + }, + "theme.blog.paginator.navAriaLabel": { + "message": "블로그 게시물 목록 탐색", + "description": "The ARIA label for the blog pagination" + }, + "theme.blog.paginator.newerEntries": { + "message": "이전 페이지", + "description": "The label used to navigate to the newer blog posts page (previous page)" + }, + "theme.blog.paginator.olderEntries": { + "message": "다음 페이지", + "description": "The label used to navigate to the older blog posts page (next page)" + }, + "theme.blog.post.paginator.navAriaLabel": { + "message": "블로그 게시물 탐색", + "description": "The ARIA label for the blog posts pagination" + }, + "theme.blog.post.paginator.newerPost": { + "message": "이전 게시물", + "description": "The blog post button label to navigate to the newer/previous post" + }, + "theme.blog.post.paginator.olderPost": { + "message": "다음 게시물", + "description": "The blog post button label to navigate to the older/next post" + }, + "theme.tags.tagsPageLink": { + "message": "모든 태그 보기", + "description": "The label of the link targeting the tag list page" + }, + "theme.colorToggle.ariaLabel": { + "message": "어두운 모드와 밝은 모드 전환하기 (현재 {mode})", + "description": "The ARIA label for the navbar color mode toggle" + }, + "theme.colorToggle.ariaLabel.mode.dark": { + "message": "어두운 모드", + "description": "The name for the dark color mode" + }, + "theme.colorToggle.ariaLabel.mode.light": { + "message": "밝은 모드", + "description": "The name for the light color mode" + }, + "theme.docs.DocCard.categoryDescription.plurals": { + "message": "{count} 항목", + "description": "The default description for a category card in the generated index about how many items this category includes" + }, + "theme.docs.breadcrumbs.navAriaLabel": { + "message": "탐색 경로", + "description": "The ARIA label for the breadcrumbs" + }, + "theme.docs.paginator.navAriaLabel": { + "message": "문서 페이지", + "description": "The ARIA label for the docs pagination" + }, + "theme.docs.paginator.previous": { + "message": "이전", + "description": "The label used to navigate to the previous doc" + }, + "theme.docs.paginator.next": { + "message": "다음", + "description": "The label used to navigate to the next doc" + }, + "theme.docs.tagDocListPageTitle.nDocsTagged": { + "message": "{count}개 문서가", + "description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.docs.tagDocListPageTitle": { + "message": "{nDocsTagged} \"{tagName}\" 태그에 분류되었습니다", + "description": "The title of the page for a docs tag" + }, + "theme.docs.versionBadge.label": { + "message": "버전: {versionLabel}" + }, + "theme.docs.versions.unreleasedVersionLabel": { + "message": "{siteTitle} {versionLabel} 문서는 아직 정식 공개되지 않았습니다.", + "description": "The label used to tell the user that he's browsing an unreleased doc version" + }, + "theme.docs.versions.unmaintainedVersionLabel": { + "message": "{siteTitle} {versionLabel} 문서는 더 이상 업데이트되지 않습니다.", + "description": "The label used to tell the user that he's browsing an unmaintained doc version" + }, + "theme.docs.versions.latestVersionSuggestionLabel": { + "message": "최신 문서는 {latestVersionLink} ({versionLabel})을 확인하세요.", + "description": "The label used to tell the user to check the latest version" + }, + "theme.docs.versions.latestVersionLinkLabel": { + "message": "최신 버전", + "description": "The label used for the latest version suggestion link label" + }, + "theme.common.editThisPage": { + "message": "페이지 편집", + "description": "The link label to edit the current page" + }, + "theme.common.headingLinkTitle": { + "message": "{heading}에 대한 직접 링크", + "description": "Title for link to heading" + }, + "theme.lastUpdated.atDate": { + "message": " {date}에", + "description": "The words used to describe on which date a page has been last updated" + }, + "theme.lastUpdated.byUser": { + "message": " {user}가", + "description": "The words used to describe by who the page has been last updated" + }, + "theme.lastUpdated.lastUpdatedAtBy": { + "message": "최종 수정: {atDate}{byUser}", + "description": "The sentence used to display when a page has been last updated, and by who" + }, + "theme.NotFound.title": { + "message": "페이지를 찾을 수 없습니다.", + "description": "The title of the 404 page" + }, + "theme.navbar.mobileVersionsDropdown.label": { + "message": "버전", + "description": "The label for the navbar versions dropdown on mobile view" + }, + "theme.tags.tagsListLabel": { + "message": "태그:", + "description": "The label alongside a tag list" + }, + "theme.admonition.caution": { + "message": "주의", + "description": "The default label used for the Caution admonition (:::caution)" + }, + "theme.admonition.danger": { + "message": "위험", + "description": "The default label used for the Danger admonition (:::danger)" + }, + "theme.admonition.info": { + "message": "정보", + "description": "The default label used for the Info admonition (:::info)" + }, + "theme.admonition.note": { + "message": "노트", + "description": "The default label used for the Note admonition (:::note)" + }, + "theme.admonition.tip": { + "message": "팁", + "description": "The default label used for the Tip admonition (:::tip)" + }, + "theme.admonition.warning": { + "message": "경고", + "description": "The default label used for the Warning admonition (:::warning)" + }, + "theme.AnnouncementBar.closeButtonAriaLabel": { + "message": "닫기", + "description": "The ARIA label for close button of announcement bar" + }, + "theme.blog.sidebar.navAriaLabel": { + "message": "최근 블로그 문서 둘러보기", + "description": "The ARIA label for recent posts in the blog sidebar" + }, + "theme.CodeBlock.wordWrapToggle": { + "message": "줄 바꿈 전환", + "description": "The title attribute for toggle word wrapping button of code block lines" + }, + "theme.CodeBlock.copied": { + "message": "복사했습니다", + "description": "The copied button label on code blocks" + }, + "theme.CodeBlock.copyButtonAriaLabel": { + "message": "클립보드에 코드 복사", + "description": "The ARIA label for copy code blocks button" + }, + "theme.CodeBlock.copy": { + "message": "복사", + "description": "The copy button label on code blocks" + }, + "theme.DocSidebarItem.expandCategoryAriaLabel": { + "message": "사이드바 분류 '{label}' 펼치기", + "description": "The ARIA label to expand the sidebar category" + }, + "theme.DocSidebarItem.collapseCategoryAriaLabel": { + "message": "사이드바 분류 '{label}' 접기", + "description": "The ARIA label to collapse the sidebar category" + }, + "theme.NavBar.navAriaLabel": { + "message": "메인", + "description": "The ARIA label for the main navigation" + }, + "theme.NotFound.p1": { + "message": "원하는 페이지를 찾을 수 없습니다.", + "description": "The first paragraph of the 404 page" + }, + "theme.NotFound.p2": { + "message": "사이트 관리자에게 링크가 깨진 것을 알려주세요.", + "description": "The 2nd paragraph of the 404 page" + }, + "theme.TOCCollapsible.toggleButtonLabel": { + "message": "이 페이지에서", + "description": "The label used by the button on the collapsible TOC component" + }, + "theme.navbar.mobileLanguageDropdown.label": { + "message": "언어", + "description": "The label for the mobile language switcher dropdown" + }, + "theme.blog.post.readMore": { + "message": "자세히 보기", + "description": "The label used in blog post item excerpts to link to full blog posts" + }, + "theme.blog.post.readMoreLabel": { + "message": "{title} 에 대해 더 읽어보기", + "description": "The ARIA label for the link to full blog posts from excerpts" + }, + "theme.blog.post.readingTime.plurals": { + "message": "약 {readingTime}분", + "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.docs.sidebar.collapseButtonTitle": { + "message": "사이드바 숨기기", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.sidebar.collapseButtonAriaLabel": { + "message": "사이드바 숨기기", + "description": "The title attribute for collapse button of doc sidebar" + }, + "theme.docs.breadcrumbs.home": { + "message": "홈", + "description": "The ARIA label for the home page in the breadcrumbs" + }, + "theme.docs.sidebar.navAriaLabel": { + "message": "문서 사이드바", + "description": "The ARIA label for the sidebar navigation" + }, + "theme.docs.sidebar.closeSidebarButtonAriaLabel": { + "message": "사이드바 닫기", + "description": "The ARIA label for close button of mobile sidebar" + }, + "theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": { + "message": "← 메인 메뉴로 돌아가기", + "description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)" + }, + "theme.docs.sidebar.expandButtonTitle": { + "message": "사이드바 열기", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.sidebar.expandButtonAriaLabel": { + "message": "사이드바 열기", + "description": "The ARIA label and title attribute for expand button of doc sidebar" + }, + "theme.docs.sidebar.toggleSidebarButtonAriaLabel": { + "message": "사이드바 펼치거나 접기", + "description": "The ARIA label for hamburger menu button of mobile navigation" + }, + "theme.blog.post.plurals": { + "message": "{count}개 게시물", + "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" + }, + "theme.blog.tagTitle": { + "message": "\"{tagName}\" 태그로 연결된 {nPosts}개의 게시물이 있습니다.", + "description": "The title of the page for a blog tag" + }, + "theme.blog.author.pageTitle": { + "message": "{authorName} - {nPosts}", + "description": "The title of the page for a blog author" + }, + "theme.blog.authorsList.pageTitle": { + "message": "저자", + "description": "The title of the authors page" + }, + "theme.blog.authorsList.viewAll": { + "message": "모든 저자 보기", + "description": "The label of the link targeting the blog authors page" + }, + "theme.blog.author.noPosts": { + "message": "작성자가 아직 게시글을 작성하지 않았습니다.", + "description": "The text for authors with 0 blog post" + }, + "theme.contentVisibility.unlistedBanner.title": { + "message": "색인되지 않은 문서", + "description": "The unlisted content banner title" + }, + "theme.contentVisibility.unlistedBanner.message": { + "message": "이 문서는 색인되지 않습니다. 검색 엔진이 이 문서를 색인하지 않으며, 주소를 알고 있는 사용자만 접근할 수 있습니다.", + "description": "The unlisted content banner message" + }, + "theme.contentVisibility.draftBanner.title": { + "message": "작성 중인 페이지", + "description": "The draft content banner title" + }, + "theme.contentVisibility.draftBanner.message": { + "message": "이 페이지는 아직 작성 중입니다. 개발 환경에서만 보이며 프로덕션 빌드에서는 제외됩니다.", + "description": "The draft content banner message" + }, + "theme.ErrorPageContent.tryAgain": { + "message": "다시 시도해 보세요", + "description": "The label of the button to try again rendering when the React error boundary captures an error" + }, + "theme.common.skipToMainContent": { + "message": "본문으로 건너뛰기", + "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" + }, + "theme.tags.tagsPageTitle": { + "message": "태그", + "description": "The title of the tag list page" + } +} diff --git a/apps/docs/i18n/ko/docusaurus-plugin-content-blog/options.json b/apps/docs/i18n/ko/docusaurus-plugin-content-blog/options.json new file mode 100644 index 000000000..9239ff706 --- /dev/null +++ b/apps/docs/i18n/ko/docusaurus-plugin-content-blog/options.json @@ -0,0 +1,14 @@ +{ + "title": { + "message": "Blog", + "description": "The title for the blog used in SEO" + }, + "description": { + "message": "Blog", + "description": "The description for the blog used in SEO" + }, + "sidebar.title": { + "message": "Recent posts", + "description": "The label for the left sidebar" + } +} diff --git a/apps/docs/i18n/ko/docusaurus-plugin-content-docs/current.json b/apps/docs/i18n/ko/docusaurus-plugin-content-docs/current.json new file mode 100644 index 000000000..48ba6ad0b --- /dev/null +++ b/apps/docs/i18n/ko/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,26 @@ +{ + "version.label": { + "message": "Next", + "description": "The label for version current" + }, + "sidebar.docs.category.소개": { + "message": "소개", + "description": "The label for category 소개 in sidebar docs" + }, + "sidebar.docs.category.사용자 가이드": { + "message": "사용자 가이드", + "description": "The label for category 사용자 가이드 in sidebar docs" + }, + "sidebar.docs.category.설정": { + "message": "설정", + "description": "The label for category 설정 in sidebar docs" + }, + "sidebar.docs.category.개발자 가이드": { + "message": "개발자 가이드", + "description": "The label for category 개발자 가이드 in sidebar docs" + }, + "sidebar.docs.category.설치": { + "message": "설치", + "description": "The label for category 설치 in sidebar docs" + } +} diff --git a/apps/docs/i18n/ko/docusaurus-theme-classic/footer.json b/apps/docs/i18n/ko/docusaurus-theme-classic/footer.json new file mode 100644 index 000000000..c7a0fb628 --- /dev/null +++ b/apps/docs/i18n/ko/docusaurus-theme-classic/footer.json @@ -0,0 +1,6 @@ +{ + "copyright": { + "message": "Copyright © 2025 ABC User Feedback.", + "description": "The footer copyright" + } +} diff --git a/apps/docs/i18n/ko/docusaurus-theme-classic/navbar.json b/apps/docs/i18n/ko/docusaurus-theme-classic/navbar.json new file mode 100644 index 000000000..492df6b4e --- /dev/null +++ b/apps/docs/i18n/ko/docusaurus-theme-classic/navbar.json @@ -0,0 +1,18 @@ +{ + "title": { + "message": "ABC User Feedback", + "description": "The title in the navbar" + }, + "logo.alt": { + "message": "LOGO", + "description": "The alt text of navbar logo" + }, + "item.label.Docs": { + "message": "문서", + "description": "Navbar item with label Docs" + }, + "item.label.GitHub": { + "message": "GitHub", + "description": "Navbar item with label GitHub" + } +} diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 000000000..4b70d285a --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,41 @@ +{ + "name": "docs", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "dev:docs": "docusaurus start", + "build:docs": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "start": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "lint": "eslint", + "typecheck": "tsc" + }, + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/plugin-google-gtag": "^3.9.2", + "@docusaurus/preset-classic": "3.9.2", + "@mdx-js/react": "^3.1.1", + "clsx": "^2.1.1", + "prism-react-renderer": "^2.4.1", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/tsconfig": "3.9.2", + "@docusaurus/types": "3.9.2", + "@ufb/eslint-config": "workspace:*", + "@ufb/prettier-config": "workspace:*", + "@ufb/tsconfig": "workspace:*", + "dotenv": "^17.2.3", + "eslint": "catalog:", + "prettier": "catalog:", + "typescript": "catalog:" + }, + "prettier": "@ufb/prettier-config" +} diff --git a/apps/docs/sidebars.ts b/apps/docs/sidebars.ts new file mode 100644 index 000000000..5a4708d5f --- /dev/null +++ b/apps/docs/sidebars.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; + +// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) + +/** + * Creating a sidebar enables you to: + - create an ordered group of docs + - render a sidebar for each doc of that group + - provide next/previous navigation + + The sidebars can be generated from the filesystem, or explicitly defined here. + + Create as many sidebars as you want. + */ +const sidebars: SidebarsConfig = { + // By default, Docusaurus generates a sidebar from the docs folder structure + docs: [{ type: 'autogenerated', dirName: '.' }], + + // But you can create a sidebar manually + /* + tutorialSidebar: [ + 'intro', + 'hello', + { + type: 'category', + label: 'Tutorial', + items: ['tutorial-basics/create-a-document'], + }, + ], + */ +}; + +export default sidebars; diff --git a/apps/docs/static/.nojekyll b/apps/docs/static/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/apps/docs/static/assets/01-feedback-tag.png b/apps/docs/static/assets/01-feedback-tag.png new file mode 100644 index 000000000..a361eaf40 Binary files /dev/null and b/apps/docs/static/assets/01-feedback-tag.png differ diff --git a/apps/docs/static/assets/02-Issue-Kanban.png b/apps/docs/static/assets/02-Issue-Kanban.png new file mode 100644 index 000000000..cd89ba6f8 Binary files /dev/null and b/apps/docs/static/assets/02-Issue-Kanban.png differ diff --git a/apps/docs/static/assets/03-issue-tracker.png b/apps/docs/static/assets/03-issue-tracker.png new file mode 100644 index 000000000..b5b9d5935 Binary files /dev/null and b/apps/docs/static/assets/03-issue-tracker.png differ diff --git a/apps/docs/static/assets/04-single-signon.png b/apps/docs/static/assets/04-single-signon.png new file mode 100644 index 000000000..b20aacc99 Binary files /dev/null and b/apps/docs/static/assets/04-single-signon.png differ diff --git a/apps/docs/static/assets/05-role-management.png b/apps/docs/static/assets/05-role-management.png new file mode 100644 index 000000000..975a3f15c Binary files /dev/null and b/apps/docs/static/assets/05-role-management.png differ diff --git a/apps/docs/static/assets/06-dashboard.png b/apps/docs/static/assets/06-dashboard.png new file mode 100644 index 000000000..054042d08 Binary files /dev/null and b/apps/docs/static/assets/06-dashboard.png differ diff --git a/apps/docs/static/assets/cover.png b/apps/docs/static/assets/cover.png new file mode 100644 index 000000000..29ddd0367 Binary files /dev/null and b/apps/docs/static/assets/cover.png differ diff --git a/apps/docs/static/assets/dashboard.png b/apps/docs/static/assets/dashboard.png new file mode 100644 index 000000000..4f6814ba6 Binary files /dev/null and b/apps/docs/static/assets/dashboard.png differ diff --git a/apps/docs/static/img/ai/ai-field-template-create.png b/apps/docs/static/img/ai/ai-field-template-create.png new file mode 100644 index 000000000..cdc9bec73 Binary files /dev/null and b/apps/docs/static/img/ai/ai-field-template-create.png differ diff --git a/apps/docs/static/img/ai/ai-field-template.png b/apps/docs/static/img/ai/ai-field-template.png new file mode 100644 index 000000000..12a63097c Binary files /dev/null and b/apps/docs/static/img/ai/ai-field-template.png differ diff --git a/apps/docs/static/img/ai/ai-issue-recommendation-create.png b/apps/docs/static/img/ai/ai-issue-recommendation-create.png new file mode 100644 index 000000000..69b316419 Binary files /dev/null and b/apps/docs/static/img/ai/ai-issue-recommendation-create.png differ diff --git a/apps/docs/static/img/ai/ai-issue-recommendation.png b/apps/docs/static/img/ai/ai-issue-recommendation.png new file mode 100644 index 000000000..1cead28e6 Binary files /dev/null and b/apps/docs/static/img/ai/ai-issue-recommendation.png differ diff --git a/apps/docs/static/img/ai/ai-setting.png b/apps/docs/static/img/ai/ai-setting.png new file mode 100644 index 000000000..5e5d3f89c Binary files /dev/null and b/apps/docs/static/img/ai/ai-setting.png differ diff --git a/apps/docs/static/img/ai/ai-usage.png b/apps/docs/static/img/ai/ai-usage.png new file mode 100644 index 000000000..106c1a786 Binary files /dev/null and b/apps/docs/static/img/ai/ai-usage.png differ diff --git a/apps/docs/static/img/api-key/api-key-detail.png b/apps/docs/static/img/api-key/api-key-detail.png new file mode 100644 index 000000000..ddfb81268 Binary files /dev/null and b/apps/docs/static/img/api-key/api-key-detail.png differ diff --git a/apps/docs/static/img/api-key/api-key-setting.png b/apps/docs/static/img/api-key/api-key-setting.png new file mode 100644 index 000000000..cd8ed7159 Binary files /dev/null and b/apps/docs/static/img/api-key/api-key-setting.png differ diff --git a/apps/docs/static/img/channel/1.png b/apps/docs/static/img/channel/1.png new file mode 100644 index 000000000..4f9f96146 Binary files /dev/null and b/apps/docs/static/img/channel/1.png differ diff --git a/apps/docs/static/img/channel/2.png b/apps/docs/static/img/channel/2.png new file mode 100644 index 000000000..dd4545a87 Binary files /dev/null and b/apps/docs/static/img/channel/2.png differ diff --git a/apps/docs/static/img/channel/3.png b/apps/docs/static/img/channel/3.png new file mode 100644 index 000000000..dd81dced7 Binary files /dev/null and b/apps/docs/static/img/channel/3.png differ diff --git a/apps/docs/static/img/channel/channel-setting.png b/apps/docs/static/img/channel/channel-setting.png new file mode 100644 index 000000000..1dffe3c83 Binary files /dev/null and b/apps/docs/static/img/channel/channel-setting.png differ diff --git a/apps/docs/static/img/channel/field-create.png b/apps/docs/static/img/channel/field-create.png new file mode 100644 index 000000000..0a28926a6 Binary files /dev/null and b/apps/docs/static/img/channel/field-create.png differ diff --git a/apps/docs/static/img/channel/field-management.png b/apps/docs/static/img/channel/field-management.png new file mode 100644 index 000000000..e08879049 Binary files /dev/null and b/apps/docs/static/img/channel/field-management.png differ diff --git a/apps/docs/static/img/docusaurus-social-card.jpg b/apps/docs/static/img/docusaurus-social-card.jpg new file mode 100644 index 000000000..ffcb44821 Binary files /dev/null and b/apps/docs/static/img/docusaurus-social-card.jpg differ diff --git a/apps/docs/static/img/docusaurus.png b/apps/docs/static/img/docusaurus.png new file mode 100644 index 000000000..f458149e3 Binary files /dev/null and b/apps/docs/static/img/docusaurus.png differ diff --git a/apps/docs/static/img/favicon.ico b/apps/docs/static/img/favicon.ico new file mode 100644 index 000000000..c01d54bcd Binary files /dev/null and b/apps/docs/static/img/favicon.ico differ diff --git a/apps/docs/static/img/feedback/0.png b/apps/docs/static/img/feedback/0.png new file mode 100644 index 000000000..4ded43107 Binary files /dev/null and b/apps/docs/static/img/feedback/0.png differ diff --git a/apps/docs/static/img/feedback/1.png b/apps/docs/static/img/feedback/1.png new file mode 100644 index 000000000..eaa2be8a2 Binary files /dev/null and b/apps/docs/static/img/feedback/1.png differ diff --git a/apps/docs/static/img/feedback/2.png b/apps/docs/static/img/feedback/2.png new file mode 100644 index 000000000..613c08f74 Binary files /dev/null and b/apps/docs/static/img/feedback/2.png differ diff --git a/apps/docs/static/img/feedback/3.png b/apps/docs/static/img/feedback/3.png new file mode 100644 index 000000000..ec5c6e006 Binary files /dev/null and b/apps/docs/static/img/feedback/3.png differ diff --git a/apps/docs/static/img/image/image-setting.png b/apps/docs/static/img/image/image-setting.png new file mode 100644 index 000000000..63f792b57 Binary files /dev/null and b/apps/docs/static/img/image/image-setting.png differ diff --git a/apps/docs/static/img/issue/1.png b/apps/docs/static/img/issue/1.png new file mode 100644 index 000000000..7cd8fa4aa Binary files /dev/null and b/apps/docs/static/img/issue/1.png differ diff --git a/apps/docs/static/img/issue/2.png b/apps/docs/static/img/issue/2.png new file mode 100644 index 000000000..11afbd69f Binary files /dev/null and b/apps/docs/static/img/issue/2.png differ diff --git a/apps/docs/static/img/logo.svg b/apps/docs/static/img/logo.svg new file mode 100644 index 000000000..589f91f93 --- /dev/null +++ b/apps/docs/static/img/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/docs/static/img/project/1.png b/apps/docs/static/img/project/1.png new file mode 100644 index 000000000..abb6e0986 Binary files /dev/null and b/apps/docs/static/img/project/1.png differ diff --git a/apps/docs/static/img/project/2.png b/apps/docs/static/img/project/2.png new file mode 100644 index 000000000..879b4f000 Binary files /dev/null and b/apps/docs/static/img/project/2.png differ diff --git a/apps/docs/static/img/project/3.png b/apps/docs/static/img/project/3.png new file mode 100644 index 000000000..e7d7dba5f Binary files /dev/null and b/apps/docs/static/img/project/3.png differ diff --git a/apps/docs/static/img/project/4.png b/apps/docs/static/img/project/4.png new file mode 100644 index 000000000..15aa6d155 Binary files /dev/null and b/apps/docs/static/img/project/4.png differ diff --git a/apps/docs/static/img/project/member-register.png b/apps/docs/static/img/project/member-register.png new file mode 100644 index 000000000..4030a1afa Binary files /dev/null and b/apps/docs/static/img/project/member-register.png differ diff --git a/apps/docs/static/img/project/member-setting.png b/apps/docs/static/img/project/member-setting.png new file mode 100644 index 000000000..9c684b759 Binary files /dev/null and b/apps/docs/static/img/project/member-setting.png differ diff --git a/apps/docs/static/img/project/project-setting.png b/apps/docs/static/img/project/project-setting.png new file mode 100644 index 000000000..a6d7ffeca Binary files /dev/null and b/apps/docs/static/img/project/project-setting.png differ diff --git a/apps/docs/static/img/project/role-create.png b/apps/docs/static/img/project/role-create.png new file mode 100644 index 000000000..3beafed3b Binary files /dev/null and b/apps/docs/static/img/project/role-create.png differ diff --git a/apps/docs/static/img/project/role-setting.png b/apps/docs/static/img/project/role-setting.png new file mode 100644 index 000000000..56743ac8f Binary files /dev/null and b/apps/docs/static/img/project/role-setting.png differ diff --git a/apps/docs/static/img/tenant.png b/apps/docs/static/img/tenant.png new file mode 100644 index 000000000..b2ff02dd6 Binary files /dev/null and b/apps/docs/static/img/tenant.png differ diff --git a/apps/docs/static/img/undraw_docusaurus_mountain.svg b/apps/docs/static/img/undraw_docusaurus_mountain.svg new file mode 100644 index 000000000..af961c49a --- /dev/null +++ b/apps/docs/static/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,171 @@ + + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/static/img/undraw_docusaurus_react.svg b/apps/docs/static/img/undraw_docusaurus_react.svg new file mode 100644 index 000000000..94b5cf08f --- /dev/null +++ b/apps/docs/static/img/undraw_docusaurus_react.svg @@ -0,0 +1,170 @@ + + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/static/img/undraw_docusaurus_tree.svg b/apps/docs/static/img/undraw_docusaurus_tree.svg new file mode 100644 index 000000000..d9161d339 --- /dev/null +++ b/apps/docs/static/img/undraw_docusaurus_tree.svg @@ -0,0 +1,40 @@ + + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/static/img/webhook/webhook-create.png b/apps/docs/static/img/webhook/webhook-create.png new file mode 100644 index 000000000..ab03df9ad Binary files /dev/null and b/apps/docs/static/img/webhook/webhook-create.png differ diff --git a/apps/docs/static/img/webhook/webhook-list.png b/apps/docs/static/img/webhook/webhook-list.png new file mode 100644 index 000000000..f3383692c Binary files /dev/null and b/apps/docs/static/img/webhook/webhook-list.png differ diff --git a/apps/docs/static/img/webhook/webhook-setting.png b/apps/docs/static/img/webhook/webhook-setting.png new file mode 100644 index 000000000..6fb6bb67d Binary files /dev/null and b/apps/docs/static/img/webhook/webhook-setting.png differ diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json new file mode 100644 index 000000000..ef0c86323 --- /dev/null +++ b/apps/docs/tsconfig.json @@ -0,0 +1,9 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "baseUrl": ".", + "strictNullChecks": true + }, + "exclude": [".docusaurus", "build"] +} diff --git a/apps/e2e/database-utils.ts b/apps/e2e/database-utils.ts deleted file mode 100644 index 3a9c3d20d..000000000 --- a/apps/e2e/database-utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -import mysql from 'mysql2/promise'; - -export async function createConnection() { - return await mysql.createConnection({ - host: '127.0.0.1', - port: 13307, - user: 'userfeedback', - password: 'userfeedback', - database: 'e2e', - }); -} diff --git a/apps/e2e/global.setup.ts b/apps/e2e/global.setup.ts index 176d73673..0b26d1481 100644 --- a/apps/e2e/global.setup.ts +++ b/apps/e2e/global.setup.ts @@ -1,54 +1,84 @@ -import { expect, test as setup } from "@playwright/test"; - -const authFile = "playwright/.auth/user.json"; - -setup("tenant create and authenticate", async ({ page }) => { - await page.goto("http://localhost:3000/tenant/create"); +import { expect, test as setup } from '@playwright/test'; + +import { initializeDatabaseForTest } from './utils/database-utils'; +import { initializeOpenSearchForTest } from './utils/opensearch-utils'; + +const authFile = 'playwright/.auth/user.json'; + +setup('tenant create and authenticate', async ({ page }) => { + try { + await initializeDatabaseForTest(); + console.log('Database initialized for test'); + } catch (error) { + console.warn( + 'Database initialization failed, continuing without database cleanup:', + error, + ); + } + + try { + await initializeOpenSearchForTest(); + console.log('OpenSearch initialized for test'); + } catch (error) { + console.warn( + 'OpenSearch initialization failed, continuing without OpenSearch:', + error, + ); + } + + await page.goto('http://localhost:3000/tenant/create', { + waitUntil: 'domcontentloaded', + }); await page.waitForTimeout(1000); await page.locator("input[name='siteName']").click(); - await page.locator("input[name='siteName']").fill("TestTenant"); - await page.getByRole("button", { name: "Next", exact: true }).click(); + await page.locator("input[name='siteName']").fill('TestTenant'); + await page.getByRole('button', { name: 'Next', exact: true }).click(); await page.waitForTimeout(1000); await page.locator("input[name='email']").click(); - await page.locator("input[name='email']").fill("user@feedback.com"); + await page.locator("input[name='email']").fill('user@feedback.com'); - await page.getByRole("button", { name: "Request Code", exact: true }).click(); + await page.getByRole('button', { name: 'Request Code', exact: true }).click(); - await page.waitForSelector('input[name="code"]', { state: "visible" }); + await page.waitForSelector('input[name="code"]', { + state: 'visible', + timeout: 60000, + }); await page.locator("input[name='code']").click(); - await page.locator("input[name='code']").fill("000000"); + await page.locator("input[name='code']").fill('000000'); - await page.getByRole("button", { name: "Verify Code", exact: true }).click(); + await page.getByRole('button', { name: 'Verify Code', exact: true }).click(); await page.locator("input[name='password']").click(); - await page.locator("input[name='password']").fill("12345678!"); + await page.locator("input[name='password']").fill('Abcd1234!'); await page.locator("input[name='confirmPassword']").click(); - await page.locator("input[name='confirmPassword']").fill("12345678!"); + await page.locator("input[name='confirmPassword']").fill('Abcd1234!'); - await page.getByRole("button", { name: "Next", exact: true }).click(); + await page.getByRole('button', { name: 'Next', exact: true }).click(); await page.waitForTimeout(1000); - await page.getByRole("button", { name: "Confirm", exact: true }).click(); + await page.getByRole('button', { name: 'Confirm', exact: true }).click(); await page.waitForTimeout(1000); - await page.goto("http://localhost:3000/auth/sign-in"); + await page.goto('http://localhost:3000/auth/sign-in', { + waitUntil: 'domcontentloaded', + }); await page.waitForTimeout(1000); - await expect(page.locator("body", { hasText: "TestTenant" })).toContainText( - "TestTenant" + await expect(page.locator('body', { hasText: 'TestTenant' })).toContainText( + 'TestTenant', ); await page.locator("input[name='email']").click(); - await page.locator("input[name='email']").fill("user@feedback.com"); + await page.locator("input[name='email']").fill('user@feedback.com'); await page.locator("input[name='password']").click(); - await page.locator("input[name='password']").fill("12345678!"); - await page.getByRole("button", { name: "Sign In", exact: true }).click(); + await page.locator("input[name='password']").fill('Abcd1234!'); + await page.getByRole('button', { name: 'Sign In', exact: true }).click(); await page.waitForTimeout(1000); - await page.waitForURL("http://localhost:3000/main/project/create"); + await page.waitForURL('http://localhost:3000/main/project/create'); await page.context().storageState({ path: authFile }); }); diff --git a/apps/e2e/global.teardown.ts b/apps/e2e/global.teardown.ts index 932224669..7561026f6 100644 --- a/apps/e2e/global.teardown.ts +++ b/apps/e2e/global.teardown.ts @@ -1,32 +1,22 @@ import { test as teardown } from '@playwright/test'; -import { createConnection } from './database-utils'; - -export async function globalTeardown() { - const connection = await createConnection(); - try { - await connection.execute(`DELETE FROM tenant WHERE site_name = ?`, [ - 'TestTenant', - ]); - await connection.execute('ALTER TABLE tenant AUTO_INCREMENT = 1'); - await connection.execute('ALTER TABLE projects AUTO_INCREMENT = 1'); - await connection.execute('ALTER TABLE channels AUTO_INCREMENT = 1'); - await connection.execute('ALTER TABLE fields AUTO_INCREMENT = 1'); - await connection.execute('ALTER TABLE feedbacks AUTO_INCREMENT = 1'); - await connection.execute(`DELETE FROM users WHERE email = ?`, [ - 'user@feedback.com', - ]); - await connection.execute('ALTER TABLE users AUTO_INCREMENT = 1'); - await connection.execute('DELETE FROM histories'); - await connection.execute('ALTER TABLE histories AUTO_INCREMENT = 1'); - } finally { - await connection.end(); - } -} +import { cleanupDatabaseAfterTest } from './utils/database-utils'; +import { cleanupOpenSearchAfterTest } from './utils/opensearch-utils'; teardown('teardown', async () => { try { - await globalTeardown(); + try { + await cleanupDatabaseAfterTest(); + } catch (error) { + console.warn('Database cleanup failed:', error); + } + + try { + await cleanupOpenSearchAfterTest(); + } catch (error) { + console.warn('OpenSearch cleanup failed:', error); + } + console.log('Tearing down succeeds.'); } catch (e) { console.log('Tearing down fails.', e); diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 0e51a91c9..974c6afd3 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -7,8 +7,9 @@ "test:e2e": "playwright test" }, "devDependencies": { - "@playwright/test": "^1.55.0", - "axios": "^1.11.0", - "mysql2": "^3.14.5" + "@opensearch-project/opensearch": "^3.5.1", + "@playwright/test": "^1.57.0", + "axios": "^1.13.2", + "mysql2": "^3.16.1" } } diff --git a/apps/e2e/playwright.config.ts b/apps/e2e/playwright.config.ts index d19fbf446..9d5657b56 100644 --- a/apps/e2e/playwright.config.ts +++ b/apps/e2e/playwright.config.ts @@ -1,28 +1,24 @@ -import * as path from "path"; -import { defineConfig, devices } from "@playwright/test"; +import * as path from 'path'; +import { defineConfig, devices } from '@playwright/test'; -export const STORAGE_STATE = path.join(__dirname, "playwright/.auth/user.json"); +import 'dotenv/config'; -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); +export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json'); /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: ".", + testDir: '.', /* Maximum time one test can run for. */ - timeout: 30 * 1000, + timeout: 60 * 1000, expect: { /** * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 15 * 1000, + timeout: 60 * 1000, }, /* Run tests in files in parallel */ fullyParallel: true, @@ -33,8 +29,8 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", - testMatch: "test.list.*", + reporter: 'html', + testMatch: 'test.list.*', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ @@ -43,25 +39,25 @@ export default defineConfig({ // baseURL: 'http://localhost:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", - video: "on", - screenshot: "only-on-failure", + trace: 'retain-on-failure', + video: 'on', + screenshot: 'only-on-failure', }, /* Configure projects for major browsers */ projects: [ { - name: "global setup", + name: 'global setup', testMatch: /global\.setup\.ts/, - teardown: "global teardown", + teardown: 'global teardown', }, { - name: "global teardown", + name: 'global teardown', testMatch: /global\.teardown\.ts/, }, { - name: "logged in chromium", - use: { ...devices["Desktop Chrome"], storageState: STORAGE_STATE }, - dependencies: ["global setup"], + name: 'logged in chromium', + use: { ...devices['Desktop Chrome'], storageState: STORAGE_STATE }, + dependencies: ['global setup'], }, // { // name: 'logged out chromium', @@ -79,34 +75,47 @@ export default defineConfig({ ], /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - outputDir: "test-results/", + outputDir: 'test-results/', /* Run your local dev server before starting the tests */ webServer: [ { - command: "cd ../api && pnpm build && pnpm start", + command: + !process.env.CI ? + 'cd ../.. && pnpm dev:api' + : 'cd ../.. && pnpm build:api && cd apps/api && pnpm start', port: 4000, - reuseExistingServer: true, + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, env: { + JWT_SECRET: 'jwtsecretjwtsecretjwtsecret', + BASE_URL: 'http://localhost:3000', MYSQL_PRIMARY_URL: - "mysql://userfeedback:userfeedback@localhost:13307/e2e", + 'mysql://userfeedback:userfeedback@localhost:13307/e2e', MYSQL_SECONDARY_URLS: '["mysql://userfeedback:userfeedback@localhost:13307/e2e"]', - AUTO_MIGRATION: "true", - MASTER_API_KEY: "MASTER_API_KEY", - NODE_ENV: "test", - SMTP_HOST: "localhost", - SMTP_PORT: "25", - SMTP_SENDER: "abc@feedback.user", - SMTP_BASE_URL: "http://localhost:3000", + AUTO_MIGRATION: 'true', + MASTER_API_KEY: 'MASTER_API_KEY', + NODE_ENV: 'test', + SMTP_HOST: 'localhost', + SMTP_PORT: '25', + SMTP_SENDER: 'abc@feedback.user', + OPENSEARCH_USE: 'true', + OPENSEARCH_NODE: 'http://localhost:9200', + OPENSEARCH_USERNAME: '', + OPENSEARCH_PASSWORD: '', }, }, { - command: "cd ../web && pnpm build && pnpm start", + command: + !process.env.CI ? + 'cd ../.. && pnpm dev:web' + : 'cd ../.. && pnpm build:web && cd apps/web && pnpm start', port: 3000, - reuseExistingServer: true, + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, env: { - NEXT_PUBLIC_API_BASE_URL: "http://localhost:4000", + NEXT_PUBLIC_API_BASE_URL: 'http://localhost:4000', }, }, ], diff --git a/apps/e2e/scenarios/create-channel.spec.ts b/apps/e2e/scenarios/create-channel.spec.ts new file mode 100644 index 000000000..9fb8b9379 --- /dev/null +++ b/apps/e2e/scenarios/create-channel.spec.ts @@ -0,0 +1,52 @@ +import { expect, test } from '@playwright/test'; + +export default () => { + test.describe('create-channel suite', () => { + test('creating a channel succeeds', async ({ page }) => { + await page.goto('http://localhost:3000', { + waitUntil: 'domcontentloaded', + }); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1000); + + await page.getByRole('radio', { name: 'Settings' }).click(); + await page.waitForTimeout(1000); + await page.getByText('Create Channel').click(); + await page.waitForTimeout(1000); + await page.locator('input[name="name"]').click(); + await page.locator('input[name="name"]').fill('TestChannel'); + await page.getByRole('button', { name: 'Next' }).click(); + + await page.getByText('Add Field').first().click(); + await page.waitForTimeout(1000); + + await page.waitForSelector('input[name="key"]', { state: 'visible' }); + await page.locator('input[name="key"]').click(); + await page.locator('input[name="key"]').fill('message'); + await page.getByRole('button', { name: 'Confirm' }).click(); + + await page.getByText('Add Field').first().click(); + await page.waitForTimeout(1000); + + await page.waitForSelector('input[name="key"]', { state: 'visible' }); + await page.locator('input[name="key"]').click(); + await page.locator('input[name="key"]').fill('image'); + + await page + .locator('select[aria-hidden="true"]') + .first() + .selectOption('images'); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'Confirm' }).click(); + + await page.getByRole('button', { name: 'Complete' }).click(); + await page.waitForTimeout(1000); + await page.getByRole('button', { name: 'Start' }).click(); + await page.waitForTimeout(1000); + await expect( + page.getByRole('tab', { name: 'TestChannel' }).first(), + ).toContainText('TestChannel'); + }); + }); +}; diff --git a/apps/e2e/scenarios/create-feedback-with-image.spec.ts b/apps/e2e/scenarios/create-feedback-with-image.spec.ts new file mode 100644 index 000000000..e65751b05 --- /dev/null +++ b/apps/e2e/scenarios/create-feedback-with-image.spec.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test'; +import axios from 'axios'; + +export default () => { + test.describe('create-feedback-with-image suite', () => { + test.afterEach(async ({ page }) => { + await page.getByText('test text').first().click(); + await page.waitForTimeout(1000); + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.getByText('Delete').nth(3)).toBeVisible(); + await page.getByText('Delete').nth(3).click(); + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).not.toContainText('test text'); + }); + + test('creating a feedback with image succeeds', async ({ page }) => { + await page.goto('http://localhost:3000', { + waitUntil: 'domcontentloaded', + }); + + await page.getByRole('radio', { name: 'Feedback' }).click(); + await page.waitForURL(/.*channelId.*/, { timeout: 10000 }); + + await expect(page.getByText('There is no data yet')).toBeVisible(); + + const url = new URL(page.url()); + const pathname = url.pathname; + const segments = pathname.split('/'); + const projectId = segments[3]; + const params = new URLSearchParams(url.search); + const channelId = params.get('channelId'); + + await axios.post( + `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + message: 'test text', + image: [ + 'https://lps-editor-2050.landpress.line.me/logo/logo_line_color.svg', + ], + }, + { headers: { 'x-api-key': 'MASTER_API_KEY' } }, + ); + + await page.goto( + `http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`, + { waitUntil: 'domcontentloaded' }, + ); + await page.waitForTimeout(1000); + + await page.getByText('Image').nth(1).click(); + await page.waitForTimeout(1000); + + await page.getByText('Cancel').nth(1).click(); + await page.waitForTimeout(1000); + }); + }); +}; diff --git a/apps/e2e/scenarios/create-feedback.spec.ts b/apps/e2e/scenarios/create-feedback.spec.ts new file mode 100644 index 000000000..32149172f --- /dev/null +++ b/apps/e2e/scenarios/create-feedback.spec.ts @@ -0,0 +1,73 @@ +import { expect, test } from '@playwright/test'; +import axios from 'axios'; +import dayjs from 'dayjs'; + +export default () => { + test.describe('create-feedback suite', () => { + test.afterEach(async ({ page }) => { + await page.getByText('test text').first().click(); + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.getByText('Delete').nth(3)).toBeVisible(); + await page.getByText('Delete').nth(3).click(); + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).not.toContainText('test text'); + }); + + test('creating a feedback succeeds', async ({ page }) => { + await page.goto('http://localhost:3000', { + waitUntil: 'domcontentloaded', + }); + + await page.getByRole('radio', { name: 'Feedback' }).click(); + await page.waitForURL(/.*channelId.*/, { timeout: 10000 }); + + await expect(page.getByText('There is no data yet')).toBeVisible(); + + const url = new URL(page.url()); + const pathname = url.pathname; + const segments = pathname.split('/'); + const projectId = segments[3]; + const params = new URLSearchParams(url.search); + const channelId = params.get('channelId'); + + await axios.post( + `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + message: 'test text', + }, + { headers: { 'x-api-key': 'MASTER_API_KEY' } }, + ); + + await page.goto( + `http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`, + { waitUntil: 'domcontentloaded' }, + ); + + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).toContainText('test text'); + + let dateSelector = page.getByText( + `${dayjs().subtract(364, 'day').format('YYYY-MM-DD')} ~ ${dayjs().format('YYYY-MM-DD')}`, + ); + + await expect(dateSelector).toBeVisible(); + await dateSelector.click(); + await page.getByText('Yesterday').click(); + await page.getByText('Save').click(); + await page.waitForTimeout(1000); + + await expect(page.getByText('There is no data yet')).toBeVisible(); + + dateSelector = page.getByText( + `${dayjs().subtract(1, 'day').format('YYYY-MM-DD')} ~ ${dayjs().subtract(1, 'day').format('YYYY-MM-DD')}`, + ); + await dateSelector.click(); + await page.getByText('Today').click(); + await page.getByText('Save').click(); + await page.waitForTimeout(1000); + }); + }); +}; diff --git a/apps/e2e/scenarios/create-issue.spec.ts b/apps/e2e/scenarios/create-issue.spec.ts new file mode 100644 index 000000000..424028aa9 --- /dev/null +++ b/apps/e2e/scenarios/create-issue.spec.ts @@ -0,0 +1,83 @@ +import { expect, test } from '@playwright/test'; +import axios from 'axios'; + +export default () => { + test.describe('create-issue suite', () => { + test.afterEach(async ({ page }) => { + await page + .getByRole('button', { name: 'Delete' }) + .click({ timeout: 1000 }); + + await expect(page.getByText('Delete').nth(3)).toBeVisible(); + await page.getByText('Delete').nth(3).click(); + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).not.toContainText('test text'); + }); + + test('creating an issue and attaching it to a feedback succeeds', async ({ + page, + }) => { + await page.goto('http://localhost:3000', { + waitUntil: 'domcontentloaded', + }); + + await page.getByRole('radio', { name: 'Issue' }).click(); + await page.getByRole('button', { name: 'Create Issue' }).click(); + await page.waitForTimeout(1000); + await page.getByPlaceholder('Please enter.').first().fill('test_issue'); + await page.getByRole('button', { name: 'Save' }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('radio', { name: 'Feedback' }).click(); + await page.waitForURL(/.*channelId.*/, { timeout: 1000 }); + + await expect(page.getByText('There is no data yet')).toBeVisible(); + + const url = new URL(page.url()); + const pathname = url.pathname; + const segments = pathname.split('/'); + const projectId = segments[3]; + const params = new URLSearchParams(url.search); + const channelId = params.get('channelId'); + + await axios.post( + `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + message: 'test text', + }, + { headers: { 'x-api-key': 'MASTER_API_KEY' } }, + ); + + await page.goto( + `http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`, + { waitUntil: 'domcontentloaded' }, + ); + + await page.waitForTimeout(2000); + + await expect(page.locator('tbody')).toContainText('test text'); + + await page.getByRole('button', { name: '+' }).first().click(); + const testIssue = page.getByText('test_issue').first(); + await expect(testIssue).toBeVisible(); + await testIssue.click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: '+' }).first().click(); + + await page.getByText('test text').first().click(); + + await page.getByRole('button', { name: '+' }).first().click(); + await page.waitForTimeout(1000); + + await page.keyboard.insertText('test_issue2'); + + await page.getByText('Create', { exact: true }).click(); + await page.waitForTimeout(1000); + + await page.keyboard.press('Enter'); + await page.waitForTimeout(1000); + }); + }); +}; diff --git a/apps/e2e/scenarios/create-multiple-feedbacks.spec.ts b/apps/e2e/scenarios/create-multiple-feedbacks.spec.ts new file mode 100644 index 000000000..d9739a6ec --- /dev/null +++ b/apps/e2e/scenarios/create-multiple-feedbacks.spec.ts @@ -0,0 +1,71 @@ +import { expect, test } from '@playwright/test'; +import axios from 'axios'; + +const NUMBER_OF_FEEDBACKS = 25; + +export default () => { + test.describe('create-multiple-feedbacks suite', () => { + test.afterEach(async ({ page }) => { + await page.getByRole('checkbox').nth(1).click(); + + await page.getByText('Delete Feedback').click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'Delete' }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('checkbox').first().click(); + + await page.getByText('Delete Feedback').click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'Delete' }).click(); + await page.waitForTimeout(1000); + }); + + test('creating multiple feedbacks succeeds', async ({ page }) => { + await page.goto('http://localhost:3000', { + waitUntil: 'domcontentloaded', + }); + + await page.getByRole('radio', { name: 'Feedback' }).click(); + await page.waitForURL(/.*channelId.*/, { timeout: 10000 }); + + await expect(page.getByText('There is no data yet')).toBeVisible(); + + const url = new URL(page.url()); + const pathname = url.pathname; + const segments = pathname.split('/'); + const projectId = segments[3]; + const params = new URLSearchParams(url.search); + const channelId = params.get('channelId'); + + for (let i = 0; i < NUMBER_OF_FEEDBACKS; i++) { + await axios.post( + `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + message: `test text ${i}`, + }, + { headers: { 'x-api-key': 'MASTER_API_KEY' } }, + ); + } + + await page.goto( + `http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`, + { waitUntil: 'domcontentloaded' }, + ); + + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).toContainText('test text'); + + await expect(page.getByText('Page 1 of 2')).toBeVisible(); + + await page.getByRole('button', { name: '20', exact: true }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('menuitem', { name: '30', exact: true }).click(); + await page.waitForTimeout(1000); + }); + }); +}; diff --git a/apps/e2e/scenarios/create-project.spec.ts b/apps/e2e/scenarios/create-project.spec.ts new file mode 100644 index 000000000..067a1bc8f --- /dev/null +++ b/apps/e2e/scenarios/create-project.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; + +export default () => { + test('creating a project succeeds', async ({ page }) => { + await page.goto('http://localhost:3000/main/project/create', { + waitUntil: 'domcontentloaded', + }); + + await expect(page.getByRole('button', { name: 'Next' })).toBeVisible({ + timeout: 1000, + }); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + + await page.locator("input[name='name']").click(); + await page.locator("input[name='name']").fill('TestProject'); + + await page.getByRole('button', { name: 'Next' }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'Next' }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'Complete' }).click(); + await page.waitForTimeout(1000); + + await expect( + page.getByRole('heading', { + level: 2, + name: /project creation complete/i, + }), + ).toBeVisible(); + await expect(page.locator("input[name='name']")).toHaveValue('TestProject'); + await page.getByRole('button', { name: 'Later' }).click(); + }); +}; diff --git a/apps/e2e/scenarios/no-database-seed/create-project.spec.ts b/apps/e2e/scenarios/no-database-seed/create-project.spec.ts deleted file mode 100644 index cac5485f4..000000000 --- a/apps/e2e/scenarios/no-database-seed/create-project.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect, test } from "@playwright/test"; - -export default () => { - test.afterEach(async ({ page }) => { - await page.goto("http://localhost:3000"); - await page.waitForTimeout(1500); - - await page.getByRole("radio", { name: "Settings" }).click(); - await page.waitForTimeout(1000); - await page.getByRole("button", { name: "Delete Project" }).click(); - await page.waitForTimeout(1000); - await page.getByRole("button", { name: "Delete" }).click(); - await page.waitForTimeout(1000); - await expect(page.getByText("TestProject")).toHaveCount(0); - }); - - test("creating a project succeeds", async ({ page }) => { - await page.goto("http://localhost:3000"); - await page.waitForTimeout(1000); - - await page.getByRole("button", { name: "Next" }).click(); - await page.getByRole("button", { name: "Next" }).click(); - await page.getByRole("button", { name: "Confirm" }).click(); - - await page.locator("input[name='name']").click(); - await page.locator("input[name='name']").fill("TestProject"); - - await page.getByRole("button", { name: "Next" }).click(); - await page.waitForTimeout(1500); - - await page.getByRole("button", { name: "Next" }).click(); - await page.waitForTimeout(1500); - - await page.getByRole("button", { name: "Complete" }).click(); - await page.waitForTimeout(1500); - - await expect(page.getByText("Project Creation Complete")).toBeVisible(); - await expect(page.locator("input[name='name']")).toHaveValue("TestProject"); - await page.getByRole("button", { name: "Later" }).click(); - }); -}; diff --git a/apps/e2e/scenarios/search-feedback.spec.ts b/apps/e2e/scenarios/search-feedback.spec.ts new file mode 100644 index 000000000..80fa5f18b --- /dev/null +++ b/apps/e2e/scenarios/search-feedback.spec.ts @@ -0,0 +1,102 @@ +import { expect, test } from '@playwright/test'; +import axios from 'axios'; + +export default () => { + test.describe('search-feedback suite', () => { + test('creating and searching a feedback succeed', async ({ page }) => { + await page.goto('http://localhost:3000', { + waitUntil: 'domcontentloaded', + }); + + await page.getByRole('radio', { name: 'Feedback' }).click(); + await page.waitForURL(/.*channelId.*/, { timeout: 10000 }); + + await expect(page.getByText('There is no data yet')).toBeVisible(); + + const url = new URL(page.url()); + const pathname = url.pathname; + const segments = pathname.split('/'); + const projectId = segments[3]; + const params = new URLSearchParams(url.search); + const channelId = params.get('channelId'); + + await axios.post( + `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + message: 'test text1', + }, + { headers: { 'x-api-key': 'MASTER_API_KEY' } }, + ); + + await axios.post( + `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, + { + message: 'test text2', + }, + { headers: { 'x-api-key': 'MASTER_API_KEY' } }, + ); + + await page.goto( + `http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}`, + { waitUntil: 'domcontentloaded' }, + ); + + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).toContainText('test text1'); + await expect(page.locator('tbody')).toContainText('test text2'); + + await page.getByText('Filter').click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'ID' }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('option', { name: 'message' }).click(); + + await page.getByPlaceholder('Please enter').fill('test text1'); + + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).toContainText('test text1'); + await expect(page.locator('tbody')).not.toContainText('test text2'); + + await page.waitForTimeout(1000); + + await page.getByText('Filter').click(); + await page.waitForTimeout(1000); + + await page.getByText('Add Filter').click(); + + await page.getByRole('button', { name: 'ID' }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('option', { name: 'message' }).click(); + + await page.getByPlaceholder('Please enter').nth(1).fill('test text2'); + + await page.getByText('And', { exact: true }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('option', { name: 'Or', exact: true }).click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', { name: 'Confirm' }).click(); + await page.waitForTimeout(1000); + + await expect(page.locator('tbody')).toContainText('test text1'); + await expect(page.locator('tbody')).toContainText('test text2'); + + await page.getByText('View').click(); + await page.getByText('ID').nth(1).click(); + await page.getByText('Created').nth(1).click(); + await page.getByText('Updated').nth(1).click(); + + await page.getByText('Expand').first().click(); + await page.waitForTimeout(1000); + await page.getByText('Expand').first().click(); + await page.waitForTimeout(1000); + }); + }); +}; diff --git a/apps/e2e/scenarios/with-database-seed/create-channel.spec.ts b/apps/e2e/scenarios/with-database-seed/create-channel.spec.ts deleted file mode 100644 index c2e5f66bd..000000000 --- a/apps/e2e/scenarios/with-database-seed/create-channel.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, test } from "@playwright/test"; - -export default () => { - test.describe("create-channel suite", () => { - test("creating a channel succeeds", async ({ page }) => { - await page.goto("http://localhost:3000"); - await page.waitForTimeout(1000); - - await page.getByRole("radio", { name: "Settings" }).click(); - await page.waitForTimeout(1000); - await page.getByRole("radio", { name: "Create Channel" }).click(); - await page.waitForTimeout(1000); - await page.locator('input[name="name"]').click(); - await page.locator('input[name="name"]').fill("TestChannel"); - await page.getByRole("button", { name: "Next" }).click(); - await page.getByRole("button", { name: "Complete" }).click(); - await page.getByRole("button", { name: "Start" }).click(); - await page.waitForTimeout(1500); - await expect( - page.getByRole("tab", { name: "TestChannel" }).first() - ).toContainText("TestChannel"); - }); - }); -}; diff --git a/apps/e2e/scenarios/with-database-seed/create-feedback.spec.ts b/apps/e2e/scenarios/with-database-seed/create-feedback.spec.ts deleted file mode 100644 index 08b888cc2..000000000 --- a/apps/e2e/scenarios/with-database-seed/create-feedback.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect, test } from "@playwright/test"; -import axios from "axios"; - -export default () => { - test.describe("create-feedback suite", () => { - test.afterEach(async ({ page }) => { - await page - .getByRole("cell", { name: "1", exact: true }) - .locator("div") - .click(); - await page.getByRole("button", { name: "Delete" }).click(); - await page.waitForTimeout(1000); - await page.getByRole("button", { name: "Delete" }).click(); - await page.waitForTimeout(1000); - - await expect(page.locator("tbody")).not.toContainText("test text"); - }); - - test("creating a feedback succeeds", async ({ page }) => { - await page.goto("http://localhost:3000"); - await page.waitForTimeout(1000); - - await page.getByRole("radio", { name: "Feedback" }).click(); - await page.waitForTimeout(1000); - await page.waitForURL(/.*channelId.*/, { timeout: 1000 }); - - const url = new URL(page.url()); - const pathname = url.pathname; - const segments = pathname.split("/"); - const projectId = segments[3]; - const params = new URLSearchParams(url.search); - const channelId = params.get("channelId"); - - await axios.post( - `http://localhost:4000/api/projects/${projectId}/channels/${channelId}/feedbacks`, - { SeededTestTextField: "test text" }, - { headers: { "x-api-key": "MASTER_API_KEY" } } - ); - - await page.goto( - `http://localhost:3000/main/project/${projectId}/feedback?channelId=${channelId}` - ); - await page.waitForTimeout(1000); - - await expect(page.locator("tbody")).toContainText("test text"); - }); - }); -}; diff --git a/apps/e2e/test.list.ts b/apps/e2e/test.list.ts index 0b1a0caf7..066f2d7dd 100644 --- a/apps/e2e/test.list.ts +++ b/apps/e2e/test.list.ts @@ -1,95 +1,19 @@ -import { test } from "@playwright/test"; -import { ResultSetHeader } from "mysql2"; +import { test } from '@playwright/test'; -import { createConnection } from "./database-utils"; -import createProject from "./scenarios/no-database-seed/create-project.spec"; -import createChannel from "./scenarios/with-database-seed/create-channel.spec"; -import createFeedback from "./scenarios/with-database-seed/create-feedback.spec"; +import createChannel from './scenarios/create-channel.spec'; +import createFeedbackWithImage from './scenarios/create-feedback-with-image.spec'; +import createFeedback from './scenarios/create-feedback.spec'; +import createIssue from './scenarios/create-issue.spec'; +import createMultipleFeedbacks from './scenarios/create-multiple-feedbacks.spec'; +import createProject from './scenarios/create-project.spec'; +import searchFeedback from './scenarios/search-feedback.spec'; -test.describe("Tests without database seed", () => { +test.describe('test suites', () => { createProject(); -}); - -test.describe("Tests with database seed", () => { - test.beforeEach(async () => { - await seedDatabase(); - }); - - test.afterEach(async () => { - await clearDatabase(); - }); - createChannel(); createFeedback(); + createFeedbackWithImage(); + createMultipleFeedbacks(); + createIssue(); + searchFeedback(); }); - -async function seedDatabase() { - const connection = await createConnection(); - try { - const projects = (await connection.execute( - `INSERT INTO projects (name, tenant_id) VALUES (?, ?)`, - ["SeededTestProject", 1] - )) as ResultSetHeader[]; - - const channels = (await connection.execute( - `INSERT INTO channels (name, project_id) VALUES (?, ?)`, - ["SeededTestChannel", projects[0].insertId] - )) as ResultSetHeader[]; - - await connection.query( - `INSERT INTO fields (\`name\`, \`key\`, \`format\`, \`property\`, \`status\`, \`channel_id\`) VALUES ?`, - [ - [ - [ - "SeededTestTextField", - "SeededTestTextField", - "text", - "READ_ONLY", - "ACTIVE", - channels[0].insertId, - ], - ["ID", "id", "number", "READ_ONLY", "ACTIVE", channels[0].insertId], - [ - "Created", - "createdAt", - "date", - "READ_ONLY", - "ACTIVE", - channels[0].insertId, - ], - [ - "Updated", - "updatedAt", - "date", - "READ_ONLY", - "ACTIVE", - channels[0].insertId, - ], - [ - "Issue", - "issues", - "multiSelect", - "EDITABLE", - "ACTIVE", - channels[0].insertId, - ], - ], - ] - ); - - console.log("seeding database succeeds"); - } finally { - await connection.end(); - } -} - -async function clearDatabase() { - const connection = await createConnection(); - try { - await connection.execute(`DELETE FROM projects WHERE name = ?`, [ - "SeededTestProject", - ]); - } finally { - await connection.end(); - } -} diff --git a/apps/e2e/utils/database-utils.ts b/apps/e2e/utils/database-utils.ts new file mode 100644 index 000000000..154f0a5d4 --- /dev/null +++ b/apps/e2e/utils/database-utils.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import mysql from 'mysql2/promise'; + +export async function createConnection() { + return await mysql.createConnection({ + host: '127.0.0.1', + port: 13307, + user: 'userfeedback', + password: 'userfeedback', + database: 'e2e', + }); +} + +export async function waitForDatabase( + maxRetries = 30, + delayMs = 1000, +): Promise { + for (let i = 0; i < maxRetries; i++) { + try { + const connection = await createConnection(); + await connection.ping(); + await connection.end(); + console.log('Database is ready'); + return; + } catch (error) { + console.log(`Waiting for database... (${i + 1}/${maxRetries})`); + if (i === maxRetries - 1) { + throw new Error( + `Database is not available after ${maxRetries} retries`, + ); + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} + +export async function clearDatabaseTables(): Promise { + const connection = await createConnection(); + + try { + await connection.execute('SET FOREIGN_KEY_CHECKS = 0'); + + const [tables] = (await connection.execute( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'e2e' AND TABLE_TYPE = 'BASE TABLE'", + )) as [mysql.RowDataPacket[], mysql.FieldPacket[]]; + + for (const table of tables) { + const tableName = table.TABLE_NAME; + if (tableName !== 'migrations') { + console.log(`Clearing table: ${tableName}`); + await connection.execute(`DELETE FROM \`${tableName}\``); + await connection.execute( + `ALTER TABLE \`${tableName}\` AUTO_INCREMENT = 1`, + ); + } + } + + await connection.execute('SET FOREIGN_KEY_CHECKS = 1'); + + console.log('Database tables cleared'); + } catch (error) { + console.log('Error clearing database tables:', error); + throw error; + } finally { + await connection.end(); + } +} + +export async function initializeDatabaseForTest(): Promise { + try { + await waitForDatabase(); + await clearDatabaseTables(); + console.log('Database initialized for test'); + } catch (error) { + console.error('Failed to initialize database:', error); + throw error; + } +} + +export async function cleanupDatabaseAfterTest(): Promise { + try { + await clearDatabaseTables(); + console.log('Database cleaned up after test'); + } catch (error) { + console.error('Failed to cleanup database:', error); + } +} diff --git a/apps/e2e/utils/opensearch-utils.ts b/apps/e2e/utils/opensearch-utils.ts new file mode 100644 index 000000000..74adfa5b7 --- /dev/null +++ b/apps/e2e/utils/opensearch-utils.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { Client } from '@opensearch-project/opensearch'; + +export interface OpenSearchConfig { + node: string; + username?: string; + password?: string; +} + +export async function createOpenSearchClient( + config: OpenSearchConfig, +): Promise { + const clientConfig: any = { + node: config.node, + }; + + if (config.username && config.password) { + clientConfig.auth = { + username: config.username, + password: config.password, + }; + } + + return new Client(clientConfig); +} + +export async function waitForOpenSearch( + client: Client, + maxRetries = 30, + delayMs = 1000, +): Promise { + for (let i = 0; i < maxRetries; i++) { + try { + await client.ping(); + console.log('OpenSearch is ready'); + return; + } catch (error) { + console.log(`Waiting for OpenSearch... (${i + 1}/${maxRetries})`); + if (i === maxRetries - 1) { + throw new Error( + `OpenSearch is not available after ${maxRetries} retries`, + ); + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} + +export async function clearOpenSearchIndices(client: Client): Promise { + try { + const response = await client.cat.indices({ format: 'json' }); + const indices = response.body as Array<{ index: string }>; + + const channelIndices = indices + .filter((index) => index.index.startsWith('channel_')) + .map((index) => index.index); + + if (channelIndices.length > 0) { + console.log(`Deleting OpenSearch indices: ${channelIndices.join(', ')}`); + await client.indices.delete({ index: channelIndices }); + console.log('OpenSearch indices cleared'); + } else { + console.log('No channel indices found in OpenSearch'); + } + } catch (error) { + console.log('Error clearing OpenSearch indices:', error); + } +} + +export async function initializeOpenSearchForTest(): Promise { + const config: OpenSearchConfig = { + node: 'http://localhost:9200', + }; + + const client = await createOpenSearchClient(config); + + try { + await waitForOpenSearch(client); + await clearOpenSearchIndices(client); + console.log('OpenSearch initialized for test'); + } catch (error) { + console.error('Failed to initialize OpenSearch:', error); + throw error; + } +} + +export async function cleanupOpenSearchAfterTest(): Promise { + const config: OpenSearchConfig = { + node: 'http://localhost:9200', + }; + + const client = await createOpenSearchClient(config); + + try { + await clearOpenSearchIndices(client); + console.log('OpenSearch cleaned up after test'); + } catch (error) { + console.error('Failed to cleanup OpenSearch:', error); + } +} diff --git a/apps/web/README.md b/apps/web/README.md index 8027fbfa2..5a569cdf3 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -61,11 +61,9 @@ pnpm build ## Environment Variables -### Required Environment Variables - -| Environment | Description | Default Value | -| ------------------------ | ------------------------------------------------------- | ------------- | -| NEXT_PUBLIC_API_BASE_URL | api base url in client side (ex. http://localhost:4000) | | +| Environment | Description | Default Value | +| ------------------------ | ------------------------------------------------------- | --------------------- | +| NEXT_PUBLIC_API_BASE_URL | api base url in client side (ex. http://localhost:4000) | http://localhost:4000 | ## Learn More diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 52e831b43..d3956e140 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +import './.next/dev/types/routes.d.ts'; // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 9541e29f3..50942e8f5 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -7,13 +7,13 @@ createJiti(fileURLToPath(import.meta.url))('./src/env'); /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, i18n: i18nConfig.default.i18n, output: 'standalone', - eslint: { ignoreDuringBuilds: true }, + typescript: { ignoreBuildErrors: true }, transpilePackages: ['@ufb/react'], compiler: { removeConsole: process.env.NODE_ENV === 'production' }, images: { remotePatterns: [{ hostname: '*' }] }, + devIndicators: false, }; export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json index 670a3090a..e751d8ad2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "build": "next build", + "build": "next build --turbopack", "clean": "git clean -xdf .next .turbo node_modules coverage .swc .cache", "dev": "next dev --turbopack", "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore", @@ -23,79 +23,79 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@faker-js/faker": "^9.9.0", - "@hookform/resolvers": "^5.2.1", + "@faker-js/faker": "^10.2.0", + "@hookform/resolvers": "^5.2.2", "@number-flow/react": "^0.5.10", "@radix-ui/react-slider": "^1.3.6", - "@t3-oss/env-nextjs": "^0.13.8", - "@tanstack/react-query": "^5.87.1", - "@tanstack/react-query-devtools": "^5.87.1", + "@t3-oss/env-nextjs": "^0.13.10", + "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query-devtools": "^5.91.2", "@tanstack/react-table": "^8.21.3", "@toss/use-overlay": "^1.4.2", "@ufb/react": "workspace:*", "@ufb/shared": "workspace:*", "@ufb/tailwindcss": "workspace:*", - "axios": "^1.11.0", + "axios": "^1.13.2", "axios-auth-refresh": "^3.3.6", "classnames": "^2.5.1", "clsx": "^2.1.1", - "cookies-next": "^6.1.0", + "cookies-next": "^6.1.1", "countries-and-timezones": "^3.8.0", "date-fns": "^4.1.0", - "dayjs": "^1.11.18", - "framer-motion": "^12.23.12", - "i18next": "^25.5.2", - "immer": "^10.1.3", + "dayjs": "^1.11.19", + "framer-motion": "^12.27.1", + "i18next": "^25.7.4", + "immer": "^11.1.3", "jwt-decode": "^4.0.0", "linkify": "^0.2.1", "linkify-react": "^4.3.2", - "next": "^15.4.7", - "next-i18next": "^15.4.2", + "next": "^16.1.3", + "next-i18next": "^15.4.3", "next-themes": "^0.4.6", "nuqs": "2.4.3", - "pino": "^9.9.4", - "react": "^19.1.1", + "pino": "^10.2.0", + "react": "^19.2.3", "react-content-loader": "^7.1.1", - "react-dom": "^19.1.1", - "react-hook-form": "^7.62.0", - "react-i18next": "^15.7.3", + "react-dom": "^19.2.3", + "react-hook-form": "^7.71.1", + "react-i18next": "^16.5.3", "react-select": "^5.10.2", "react-use": "^17.6.0", - "recharts": "^2.15.4", - "sharp": "^0.34.3", - "swiper": "^11.2.10", - "tailwind-merge": "^3.3.1", + "recharts": "^3.5.0", + "sharp": "^0.34.5", + "swiper": "^12.0.3", + "tailwind-merge": "^3.4.0", "tailwind-scrollbar-hide": "^4.0.0", - "zod": "^4.1.5", - "zustand": "^5.0.8" + "zod": "^4.3.5", + "zustand": "^5.0.10" }, "devDependencies": { - "@babel/core": "^7.28.4", - "@rollup/plugin-commonjs": "^28.0.6", + "@babel/core": "^7.28.6", + "@rollup/plugin-commonjs": "^29.0.0", "@svgr/webpack": "^8.1.0", "@swc/core": "1.13.5", "@swc/jest": "^0.2.39", - "@testing-library/jest-dom": "^6.8.0", - "@testing-library/react": "^16.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/jest": "^30.0.0", - "@types/node": "22.18.1", - "@types/react": "^19.1.12", - "@types/react-dom": "^19.1.9", + "@types/node": "24.10.8", + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", "@ufb/eslint-config": "workspace:*", "@ufb/prettier-config": "workspace:*", "@ufb/tsconfig": "workspace:*", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.23", "eslint": "catalog:", - "jest": "^30.1.3", - "jest-environment-jsdom": "^30.1.2", - "jest-fixed-jsdom": "^0.0.10", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", + "jest-fixed-jsdom": "^0.0.11", "jiti": "1.21.7", - "next-router-mock": "^1.0.2", - "openapi-typescript": "^7.9.1", + "next-router-mock": "^1.0.5", + "openapi-typescript": "^7.10.1", "postcss": "^8.5.6", "prettier": "catalog:", - "tailwindcss": "^3.4.17", + "tailwindcss": "^3.4.19", "ts-toolbelt": "^9.6.0", "typescript": "catalog:" }, diff --git a/apps/web/src/entities/dashboard/ui/feedback-line-chart.ui.tsx b/apps/web/src/entities/dashboard/ui/feedback-line-chart.ui.tsx index cfade7c74..7d51debc5 100644 --- a/apps/web/src/entities/dashboard/ui/feedback-line-chart.ui.tsx +++ b/apps/web/src/entities/dashboard/ui/feedback-line-chart.ui.tsx @@ -26,7 +26,13 @@ import { Icon, } from '@ufb/react'; -import { SimpleLineChart, useAllChannels, useOAIQuery } from '@/shared'; +import { + ChartCard, + Legend, + LineChart, + useAllChannels, + useOAIQuery, +} from '@/shared'; import type { Channel } from '@/entities/channel'; import { useLineChartData } from '../lib'; @@ -73,47 +79,65 @@ const FeedbackLineChartWrapper: React.FC = (props) => { ); return ( - - - - Filter - - - - {channels?.items.map((channel) => ( - id === channel.id)} - onSelect={() => { - const isChecked = currentChannels.some( - ({ id }) => id === channel.id, - ); - setCurrentChannels((prev) => - isChecked ? prev.filter(({ id }) => id !== channel.id) - : prev.length === 5 ? [...prev.slice(1), channel] - : [...prev, channel], - ); - }} - > - {channel.name} - - ))} - - - + extra={ +
+ + +
} - showLegend - /> + > + + + ); +}; + +interface ChannelSelectComboboxProps { + channels?: { items: Channel[] }; + currentChannels: Channel[]; + setCurrentChannels: React.Dispatch>; +} + +const ChannelSelectCombobox = (props: ChannelSelectComboboxProps) => { + const { channels, currentChannels, setCurrentChannels } = props; + return ( + + + + Filter + + + + {channels?.items.map((channel) => ( + id === channel.id)} + onSelect={() => { + const isChecked = currentChannels.some( + ({ id }) => id === channel.id, + ); + setCurrentChannels((prev) => + isChecked ? prev.filter(({ id }) => id !== channel.id) + : prev.length === 5 ? [...prev.slice(1), channel] + : [...prev, channel], + ); + }} + > + {channel.name} + + ))} + + + ); }; diff --git a/apps/web/src/entities/dashboard/ui/issue-bar-chart.ui.tsx b/apps/web/src/entities/dashboard/ui/issue-bar-chart.ui.tsx index 1785cd497..e1684ed50 100644 --- a/apps/web/src/entities/dashboard/ui/issue-bar-chart.ui.tsx +++ b/apps/web/src/entities/dashboard/ui/issue-bar-chart.ui.tsx @@ -19,10 +19,11 @@ import { useTranslation } from 'next-i18next'; import { ToggleGroup, ToggleGroupItem } from '@ufb/react'; import { + BarChart, + ChartCard, ISSUES, Path, - SimpleBarChart, - SimplePieChart, + PieChart, useOAIQuery, } from '@/shared'; import type { IssueStatus } from '@/entities/issue'; @@ -44,7 +45,7 @@ const IssueBarChart: React.FC = ({ projectId }) => { const { t } = useTranslation(); const [type, setType] = useState('bar'); - const { data } = useOAIQuery({ + const { data: statisticsData } = useOAIQuery({ path: '/api/admin/statistics/issue/count-by-status', variables: { projectId }, queryOptions: { @@ -57,82 +58,89 @@ const IssueBarChart: React.FC = ({ projectId }) => { const onChangeType = (type: 'bar' | 'pie') => (v: string) => { setType(v === '' ? type : v); }; + const data = ISSUES(t).map(({ key, name }) => ({ + name, + value: +( + statisticsData?.statistics.find((v) => v.status === key)?.count ?? 0 + ), + color: COLOR_MAP[key], + })); + const onClickIssue = (name: string | undefined) => { + if (!name) return; + const issue = ISSUES(t).find((v) => v.name === name); + window.open( + Path.ISSUE.replace('[projectId]', projectId.toString()) + + '?queries=' + + JSON.stringify([{ key: 'status', value: issue?.key, condition: 'IS' }]), + '_blank', + ); + }; return ( - <> + + Bar + Pie + + } + > {type === 'pie' && ( - ({ - name, - value: +( - data?.statistics.find((v) => v.status === key)?.count ?? 0 - ), - color: COLOR_MAP[key], - }))} - title={t('chart.issue-status-count.title')} - description={t('chart.issue-status-count.description')} - height={415} - filterContent={ - - Bar - Pie - - } - onClick={(data) => { - if (!data) return; - const issue = ISSUES(t).find((v) => v.name === data.name); - window.open( - Path.ISSUE.replace('[projectId]', projectId.toString()) + - '?queries=' + - JSON.stringify([ - { key: 'status', value: issue?.key, condition: 'IS' }, - ]), - '_blank', - ); - }} - /> +
+ + +
)} {type === 'bar' && ( - ({ - name, - value: +( - data?.statistics.find((v) => v.status === key)?.count ?? 0 - ), - color: COLOR_MAP[key], - }))} - title={t('chart.issue-status-count.title')} - description={t('chart.issue-status-count.description')} - height={415} - filterContent={ - - Bar - Pie - - } - onClick={(data) => { - if (!data) return; - const issue = ISSUES(t).find((v) => v.name === data.name); - window.open( - Path.ISSUE.replace('[projectId]', projectId.toString()) + - '?queries=' + - JSON.stringify([ - { key: 'status', value: issue?.key, condition: 'IS' }, - ]), - '_blank', - ); - }} - /> + )} - +
+ ); +}; +interface IssueLegendProps { + data?: { name: string; value: number; color: string }[]; +} + +const IssueLegend: React.FC = ({ data }) => { + return ( +
+ + + + + + + + + {data?.map((item, index) => ( + + + + + ))} + +
+ Status + + Issue Count +
+
+ + {item.name} +
+
+ {item.value} +
+
); }; diff --git a/apps/web/src/entities/dashboard/ui/issue-feedback-line-chart.ui.tsx b/apps/web/src/entities/dashboard/ui/issue-feedback-line-chart.ui.tsx index 9953a96ea..3ae3bd238 100644 --- a/apps/web/src/entities/dashboard/ui/issue-feedback-line-chart.ui.tsx +++ b/apps/web/src/entities/dashboard/ui/issue-feedback-line-chart.ui.tsx @@ -32,11 +32,13 @@ import { } from '@ufb/react'; import { + ChartCard, client, commandFilter, getDayCount, InfiniteScrollArea, - SimpleLineChart, + Legend, + LineChart, useOAIQuery, } from '@/shared'; import type { Issue } from '@/entities/issue'; @@ -130,77 +132,79 @@ const IssueFeedbackLineChart: React.FC = ({ from, projectId, to }) => { }; return ( - - - - Filter - - - setSearchName(value)} - value={searchName} - /> - - No results found. - - Selected Issue - - } - > - {currentIssues.map((issue) => ( - handleIssueUncheck(issue)} - > - - - ))} - - {allIssues.length > 0 && ( + extra={ +
+ + + + + Filter + + + setSearchName(value)} + value={searchName} + /> + + No results found. - Issue List + Selected Issue } > - {allIssues - .filter( - (v) => !currentIssues.some((issue) => issue.id === v.id), - ) - .map((issue) => ( - handleIssueCheck(issue)} - > - - - ))} - + {currentIssues.map((issue) => ( + handleIssueUncheck(issue)} + > + + + ))} - )} - - - + {allIssues.length > 0 && ( + + Issue List + + } + > + {allIssues + .filter( + (v) => + !currentIssues.some((issue) => issue.id === v.id), + ) + .map((issue) => ( + handleIssueCheck(issue)} + > + + + ))} + + + )} + + + +
} - /> + > + + ); }; diff --git a/apps/web/src/entities/dashboard/ui/issue-line-chart.ui.tsx b/apps/web/src/entities/dashboard/ui/issue-line-chart.ui.tsx index 8dde69094..a05d9e2d7 100644 --- a/apps/web/src/entities/dashboard/ui/issue-line-chart.ui.tsx +++ b/apps/web/src/entities/dashboard/ui/issue-line-chart.ui.tsx @@ -16,7 +16,7 @@ import dayjs from 'dayjs'; import { useTranslation } from 'next-i18next'; -import { getDayCount, SimpleLineChart, useOAIQuery } from '@/shared'; +import { ChartCard, getDayCount, LineChart, useOAIQuery } from '@/shared'; import { useLineChartData } from '../lib'; @@ -53,15 +53,14 @@ const IssueLineChart: React.FC = ({ from, projectId, to }) => { ); return ( - + > + + ); }; diff --git a/apps/web/src/entities/feedback/lib/use-feedback-query-converter.ts b/apps/web/src/entities/feedback/lib/use-feedback-query-converter.ts index e6222f10c..7d7218600 100644 --- a/apps/web/src/entities/feedback/lib/use-feedback-query-converter.ts +++ b/apps/web/src/entities/feedback/lib/use-feedback-query-converter.ts @@ -14,7 +14,7 @@ * under the License. */ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import dayjs from 'dayjs'; import { createParser, parseAsStringLiteral, useQueryState } from 'nuqs'; @@ -161,7 +161,7 @@ const useFeedbackQueryConverter = (input: { .filter((v) => !!v) as TableFilter[]; }, [queries, filterFields]); - const onChageTableFilters = useCallback( + const onChangeTableFilters = useCallback( async (tableFilters: TableFilter[], operator: TableFilterOperator) => { const result = await Promise.all( tableFilters.map(async ({ condition, format, key, value }) => ({ @@ -184,20 +184,37 @@ const useFeedbackQueryConverter = (input: { [dateRange], ); + const filteredQueries = queries.filter((query) => + filterFields.some((field) => field.key === query.key), + ); + + const filteredTableFilters = tableFilters.filter((v) => + filterFields.some((vv) => vv.key === v.key), + ); + + useEffect(() => { + const diff = dayjs(dateRange?.endDate).diff( + dayjs(dateRange?.startDate), + 'day', + ); + if (diff >= feedbackSearchMaxDays) { + void onChangeDateRange({ + startDate: dayjs() + .subtract(feedbackSearchMaxDays - 1, 'day') + .toDate(), + endDate: dayjs().toDate(), + }); + } + }, [feedbackSearchMaxDays]); + return { - queries: queries - .filter((query) => filterFields.some((field) => field.key === query.key)) - .filter((v) => !!v.value), + queries: filteredQueries, defaultQueries, - tableFilters: tableFilters.filter((v) => - filterFields.some((vv) => vv.key === v.key), - ), + tableFilters: filteredTableFilters, operator, dateRange, - updateTableFilters: onChageTableFilters, - updateDateRage: onChangeDateRange, - resetDateRange: () => - setDefaultQueries(defaultDateRange ? [defaultDateRange] : []), + updateTableFilters: onChangeTableFilters, + updateDateRange: onChangeDateRange, }; }; diff --git a/apps/web/src/entities/feedback/ui/feedback-table.ui.tsx b/apps/web/src/entities/feedback/ui/feedback-table.ui.tsx index b72fb8d7c..ec378143a 100644 --- a/apps/web/src/entities/feedback/ui/feedback-table.ui.tsx +++ b/apps/web/src/entities/feedback/ui/feedback-table.ui.tsx @@ -30,15 +30,7 @@ import { import { useOverlay } from '@toss/use-overlay'; import { useTranslation } from 'next-i18next'; -import { - Badge, - Button, - Icon, - toast, - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@ufb/react'; +import { Badge, Button, Icon, toast } from '@ufb/react'; import type { TableFilterField } from '@/shared'; import { @@ -49,6 +41,7 @@ import { TablePagination, useOAIMutation, usePermissions, + useSort, } from '@/shared'; import type { Channel } from '@/entities/channel'; import type { Field } from '@/entities/field'; @@ -74,7 +67,6 @@ interface Props { filterFields: TableFilterField[]; fields: Field[]; } - const FeedbackTable = (props: Props) => { const { channels, @@ -87,11 +79,7 @@ const FeedbackTable = (props: Props) => { const { t } = useTranslation(); const perms = usePermissions(); - const overlay = useOverlay(); - const setLoadingFeedbackIds = useAIFIeldFeedbackCellLoading( - (state) => state.setLoadingFeedbackIds, - ); const [openFeedbackId, setOpenFeedbackId] = useState(null); const [rows, setRows] = useState[]>([]); @@ -129,15 +117,32 @@ const FeedbackTable = (props: Props) => { dateRange, operator, updateTableFilters, - updateDateRage, + updateDateRange, defaultQueries, - resetDateRange, } = useFeedbackQueryConverter({ projectId, filterFields, feedbackSearchMaxDays: currentChannel.feedbackSearchMaxDays, }); + const onChangeCurrentChannelId = (channelId: number) => { + setCurrentChannelId(channelId); + setPagination((prev) => ({ ...prev, pageIndex: 0 })); + table.resetRowSelection(); + }; + + const onChangeTableFilters: typeof updateTableFilters = async (...input) => { + await updateTableFilters(...input); + setPagination((prev) => ({ ...prev, pageIndex: 0 })); + table.resetRowSelection(); + }; + + const onChangeDateRange: typeof updateDateRange = async (...input) => { + await updateDateRange(...input); + setPagination((prev) => ({ ...prev, pageIndex: 0 })); + table.resetRowSelection(); + }; + const columns = useMemo(() => getColumns(fields), [fields]); const table = useReactTable({ @@ -159,7 +164,9 @@ const FeedbackTable = (props: Props) => { onColumnVisibilityChange: onChangeColumnVisibility, getRowId: (row) => String(row.id), }); - const { rowSelection } = table.getState(); + + const { rowSelection, sorting } = table.getState(); + const sort = useSort(sorting); const selectedRowIds = useMemo(() => { return Object.entries(rowSelection).reduce( @@ -178,11 +185,13 @@ const FeedbackTable = (props: Props) => { { queries, operator, + sort, page: pagination.pageIndex + 1, limit: pagination.pageSize, defaultQueries, }, { + staleTime: 1000 * 60, throwOnError(error) { if (error.code === 'LargeWindow') { toast.error('Please narrow down the search range.'); @@ -191,9 +200,6 @@ const FeedbackTable = (props: Props) => { }, }, ); - useEffect(() => { - void resetDateRange(); - }, [currentChannel]); useEffect(() => { setRows(feedbackData?.items ?? []); @@ -202,11 +208,6 @@ const FeedbackTable = (props: Props) => { table.resetRowSelection(); }, [feedbackData]); - useEffect(() => { - table.setPageIndex(0); - table.resetRowSelection(); - }, [pagination.pageSize]); - const { mutateAsync: deleteFeedback } = useOAIMutation({ method: 'delete', path: '/api/admin/projects/{projectId}/channels/{channelId}/feedbacks', @@ -219,15 +220,6 @@ const FeedbackTable = (props: Props) => { }, }, }); - const openDeleteFeedbacksDialog = () => { - overlay.open(({ close, isOpen }) => ( - deleteFeedback({ feedbackIds: selectedRowIds })} - /> - )); - }; const currentFeedback = useMemo( () => rows.find((v) => v.id === openFeedbackId), @@ -250,87 +242,44 @@ const FeedbackTable = (props: Props) => { }, }); - const { mutateAsync: processAI, isPending: isPendingAIProcess } = - useOAIMutation({ - method: 'post', - path: '/api/admin/projects/{projectId}/ai/process', - pathParams: { projectId }, - queryOptions: { - onSuccess() { - toast.success(t('v2.toast.success')); - }, - async onSettled() { - table.resetRowSelection(); - await refetch(); - setLoadingFeedbackIds([]); - }, - onError(error) { - toast.error(error.message); - }, - }, - }); - return (
{selectedRowIds.length > 0 && ( <> - + {fields.filter( (v) => v.format === 'aiField' && v.status === 'ACTIVE', ).length > 0 && ( - + /> )} )} - - - - - - {t('tooltip.feedback-date-picker-button')} - - + @@ -338,7 +287,7 @@ const FeedbackTable = (props: Props) => { table.getAllColumns().some((column) => column.id === v.key), )} operator={operator} - onSubmit={updateTableFilters} + onSubmit={onChangeTableFilters} tableFilters={tableFilters} table={table} /> @@ -380,5 +329,86 @@ const FeedbackTable = (props: Props) => {
); }; +const FeedbackDeleteButton = (props: { + selectedRowIds: number[]; + deleteFeedback: (params: { feedbackIds: number[] }) => Promise; +}) => { + const { selectedRowIds, deleteFeedback } = props; + const { t } = useTranslation(); + const perms = usePermissions(); + const overlay = useOverlay(); + const openDeleteFeedbacksDialog = () => { + overlay.open(({ close, isOpen }) => ( + deleteFeedback({ feedbackIds: selectedRowIds })} + /> + )); + }; + return ( + + ); +}; + +const RunAIButton = (props: { + selectedRowIds: number[]; + projectId: number; + afterProcess: () => void; +}) => { + const { selectedRowIds, projectId, afterProcess } = props; + const { t } = useTranslation(); + const perms = usePermissions(); + const setLoadingFeedbackIds = useAIFIeldFeedbackCellLoading( + (state) => state.setLoadingFeedbackIds, + ); + + const { mutateAsync: processAI, isPending: isPendingAIProcess } = + useOAIMutation({ + method: 'post', + path: '/api/admin/projects/{projectId}/ai/process', + pathParams: { projectId }, + queryOptions: { + onSuccess() { + toast.success(t('v2.toast.success')); + }, + onSettled() { + afterProcess(); + setLoadingFeedbackIds([]); + }, + onError(error) { + toast.error(error.message); + }, + }, + }); + return ( + + ); +}; export default FeedbackTable; diff --git a/apps/web/src/entities/feedback/ui/issue-cell.tsx b/apps/web/src/entities/feedback/ui/issue-cell.tsx index fad93aeb1..27739c317 100644 --- a/apps/web/src/entities/feedback/ui/issue-cell.tsx +++ b/apps/web/src/entities/feedback/ui/issue-cell.tsx @@ -14,6 +14,7 @@ * under the License. */ import { useMemo, useState } from 'react'; +import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; @@ -37,7 +38,6 @@ import { client, cn, commandFilter, - InfiniteScrollArea, useOAIMutation, usePermissions, } from '@/shared'; @@ -47,6 +47,11 @@ import type { Issue } from '@/entities/issue'; import { useFeedbackSearch } from '../lib'; import AiIssueComboboxGroup from './ai-issue-combobox-group.ui'; +const InfiniteScrollArea = dynamic( + () => import('@/shared/ui/infinite-scroll-area.ui'), + { ssr: false }, +); + interface IProps { issues?: Issue[]; feedbackId?: number; diff --git a/apps/web/src/entities/member/ui/member-form-dialog.ui.tsx b/apps/web/src/entities/member/ui/member-form-dialog.ui.tsx index 1b1a26bdd..7f71cfb3c 100644 --- a/apps/web/src/entities/member/ui/member-form-dialog.ui.tsx +++ b/apps/web/src/entities/member/ui/member-form-dialog.ui.tsx @@ -135,7 +135,7 @@ const MemberFormDialog: React.FC = (props) => { ({ label: v.name, value: `${v.id}` }))} onChange={(v) => { const role = roles.find((role) => String(role.id) === v); diff --git a/apps/web/src/entities/tenant/ui/oauth-config-form.ui.tsx b/apps/web/src/entities/tenant/ui/oauth-config-form.ui.tsx index 3f9afa249..873b0ed3e 100644 --- a/apps/web/src/entities/tenant/ui/oauth-config-form.ui.tsx +++ b/apps/web/src/entities/tenant/ui/oauth-config-form.ui.tsx @@ -62,7 +62,7 @@ const OAuthConfigForm: React.FC = ({ disabled }) => { /> = (props) => { const { register, watch, setValue, handleSubmit, formState } = useForm({ resolver: zodResolver(scheme), defaultValues }); - const { type, projectId } = watch(); + const { type, projectId, roleId } = watch(); const { data: projectData } = useAllProjects(); @@ -99,14 +99,18 @@ const InviteUserDialog: React.FC = (props) => { /> - setValue('type', v as UserTypeEnum, { shouldDirty: true }) - } + onChange={(v) => { + setValue('type', v as UserTypeEnum, { shouldDirty: true }); + if (type === 'SUPER') { + setValue('projectId', 0); + setValue('roleId', 0); + } + }} required /> {type === 'GENERAL' && ( @@ -118,13 +122,15 @@ const InviteUserDialog: React.FC = (props) => { label: name, value: id.toString(), }))} - onChange={(v) => - setValue('projectId', Number(v), { shouldDirty: true }) - } + onChange={(v) => { + setValue('projectId', Number(v), { shouldDirty: true }); + setValue('roleId', 0, { shouldDirty: true }); + }} + value={projectId === 0 ? '' : projectId.toString()} error={formState.errors.projectId?.message} required /> - {projectId && ( + {projectId > 0 && ( = (props) => { onChange={(v) => setValue('roleId', Number(v), { shouldDirty: true }) } + value={roleId === 0 ? '' : roleId.toString()} error={formState.errors.roleId?.message} required /> diff --git a/apps/web/src/entities/user/user.schema.ts b/apps/web/src/entities/user/user.schema.ts index 1cea90be0..d1518d3a0 100644 --- a/apps/web/src/entities/user/user.schema.ts +++ b/apps/web/src/entities/user/user.schema.ts @@ -18,7 +18,7 @@ import { z } from 'zod'; export const userSchema = z.object({ id: z.number(), - email: z.string().email(), + email: z.email(), type: z.union([z.literal('SUPER'), z.literal('GENERAL')]), name: z.string().max(20).trim().nullable(), department: z.string().max(50).trim().nullable(), @@ -91,7 +91,7 @@ export const invitedUserSignupSchema = z password: z.string().min(8), confirmPassword: z.string().min(8), code: z.string(), - email: z.string().email(), + email: z.email(), }) .refine(({ password, confirmPassword }) => password === confirmPassword, { message: 'must equal Password', diff --git a/apps/web/src/entities/webhook/ui/webhook-event-cell.tsx b/apps/web/src/entities/webhook/ui/webhook-event-cell.tsx index 14dfc44f8..fd634e571 100644 --- a/apps/web/src/entities/webhook/ui/webhook-event-cell.tsx +++ b/apps/web/src/entities/webhook/ui/webhook-event-cell.tsx @@ -57,7 +57,9 @@ const WebhookEventCell: React.FC = (props) => { {(type === 'ISSUE_CREATION' || type === 'ISSUE_STATUS_CHANGE' ? data?.items : channels - )?.map(({ id, name }) => {name})} + )?.map(({ id, name }) => ( + {name} + ))} ); diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 2926f92a0..8cb5ac92e 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -23,7 +23,7 @@ export const env = createEnv({ .default('development'), }, client: { - NEXT_PUBLIC_API_BASE_URL: z.string().url(), + NEXT_PUBLIC_API_BASE_URL: z.string(), }, runtimeEnv: { NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, diff --git a/apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts b/apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts index ac48d54ed..f2949f365 100644 --- a/apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts +++ b/apps/web/src/features/auth/reset-password-with-email/request-reset-password-with-email.schema.ts @@ -16,5 +16,5 @@ import { z } from 'zod'; export const requestResetPasswordWithEmailSchema = z.object({ - email: z.string().email(), + email: z.email(), }); diff --git a/apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts b/apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts index 0d6e965af..e6b9f01c1 100644 --- a/apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts +++ b/apps/web/src/features/auth/reset-password-with-email/reset-password-with-email.schema.ts @@ -20,7 +20,7 @@ export const resetPasswordWithEmailSchema = z password: z.string().min(8), confirmPassword: z.string().min(8), code: z.string(), - email: z.string().email(), + email: z.email(), }) .refine(({ password, confirmPassword }) => password === confirmPassword, { message: 'must equal Password', diff --git a/apps/web/src/features/auth/sign-up-with-email/sign-up-with-email.schema.ts b/apps/web/src/features/auth/sign-up-with-email/sign-up-with-email.schema.ts index 4b340dfe7..9ae267572 100644 --- a/apps/web/src/features/auth/sign-up-with-email/sign-up-with-email.schema.ts +++ b/apps/web/src/features/auth/sign-up-with-email/sign-up-with-email.schema.ts @@ -18,7 +18,7 @@ import { z } from 'zod'; // export const signUpWithEmailSchema = z .object({ - email: z.string().email(), + email: z.email(), emailState: z.enum(['NOT_VERIFIED', 'VERIFING', 'EXPIRED', 'VERIFIED']), code: z.string().length(6), password: z.string().min(8), diff --git a/apps/web/src/features/update-ai-setting/ui/ai-field-template-form.ui.css b/apps/web/src/features/update-ai-setting/ui/ai-field-template-form.ui.css new file mode 100644 index 000000000..e69de29bb diff --git a/apps/web/src/features/update-ai-setting/ui/ai-usage.ui.tsx b/apps/web/src/features/update-ai-setting/ui/ai-usage.ui.tsx index a95e2aa73..7559c8005 100644 --- a/apps/web/src/features/update-ai-setting/ui/ai-usage.ui.tsx +++ b/apps/web/src/features/update-ai-setting/ui/ai-usage.ui.tsx @@ -39,10 +39,12 @@ import { CardDescription, CardHeader, CardTitle, + ChartCard, DateRangePicker, + Legend, + LineChart, SelectInput, SettingAlert, - SimpleLineChart, useOAIMutation, useOAIQuery, useWarnIfUnsavedChanges, @@ -280,6 +282,12 @@ export const AIUsageFormButton = () => { ); }; +const dataKeys = [ + { name: 'Total', color: '#4A90E2' }, + { name: 'AI Field', color: '#50E3C2' }, + { name: 'AI Issue', color: '#F5A623' }, +]; + const AIChartCard = ({ projectId }: { projectId: number }) => { const [dateRange, setDateRange] = useState({ startDate: dayjs().startOf('month').toDate(), @@ -334,25 +342,22 @@ const AIChartCard = ({ projectId }: { projectId: number }) => { }, [dailyData, dateRange]); return ( - + extra={ +
+ + +
} - showLegend - /> + > + + ); }; const AIChartUsageCard = ({ projectId }: { projectId: number }) => { @@ -418,30 +423,46 @@ const AIChartUsageCard = ({ projectId }: { projectId: number }) => { height={180} > value.toLocaleString()} + formatter={(value: unknown) => { + if (typeof value === 'number') { + return value.toLocaleString(); + } + return String(value); + }} cursor={{ fill: 'var(--bg-neutral-tertiary)' }} content={({ payload }) => { return (
- {payload?.map(({ value, payload }, i) => ( -
-
-
-

- {(payload as { name: string | undefined }).name} + {payload.map((item, i) => { + const { value, payload: itemPayload } = item as { + value: unknown; + payload: unknown; + }; + return ( +

+
+
+

+ {(itemPayload as { name: string | undefined }).name} +

+
+

+ {typeof value === 'number' ? + value.toLocaleString() + : String(value)}

-

{value?.toLocaleString()}

-
- ))} + ); + })}
); }} @@ -467,7 +488,7 @@ const AIChartUsageCard = ({ projectId }: { projectId: number }) => { > {integrationData?.tokenThreshold ? @@ -476,7 +497,7 @@ const AIChartUsageCard = ({ projectId }: { projectId: number }) => { Remaining Tokens diff --git a/apps/web/src/pages/auth/sign-in.tsx b/apps/web/src/pages/auth/sign-in.tsx index 24c73a173..a448e0b7e 100644 --- a/apps/web/src/pages/auth/sign-in.tsx +++ b/apps/web/src/pages/auth/sign-in.tsx @@ -33,7 +33,7 @@ import { AnonymousLayout } from '@/widgets/anonymous-layout'; import serverSideTranslations from '@/server-side-translations'; const signInWithEmailSchema = z.object({ - email: z.string().email(), + email: z.email(), password: z.string().min(8), }); diff --git a/apps/web/src/pages/main/project/[projectId]/dashboard.tsx b/apps/web/src/pages/main/project/[projectId]/dashboard.tsx index ee6e11fcc..cac6f229b 100644 --- a/apps/web/src/pages/main/project/[projectId]/dashboard.tsx +++ b/apps/web/src/pages/main/project/[projectId]/dashboard.tsx @@ -27,9 +27,6 @@ import { ComboboxSelectItem, ComboboxTrigger, Icon, - Tooltip, - TooltipContent, - TooltipTrigger, } from '@ufb/react'; import { DateRangePicker, parseAsDateRange } from '@/shared'; @@ -166,20 +163,14 @@ const DashboardPage: NextPageWithLayout = ({ projectId }) => {
- - - - - - {t('tooltip.dashboard-date-picker-button')} - - + diff --git a/apps/web/src/pages/main/project/[projectId]/feedback.tsx b/apps/web/src/pages/main/project/[projectId]/feedback.tsx index 20a082f7e..f63e01906 100644 --- a/apps/web/src/pages/main/project/[projectId]/feedback.tsx +++ b/apps/web/src/pages/main/project/[projectId]/feedback.tsx @@ -26,6 +26,7 @@ import type { TableFilterField } from '@/shared'; import { useAllChannels, useOAIQuery } from '@/shared'; import type { NextPageWithLayout } from '@/shared/types'; import { useCheckAIUsageLimit } from '@/entities/ai'; +import type { Channel } from '@/entities/channel'; import { useRoutingChannelCreation } from '@/entities/channel/lib'; import { FeedbackTable } from '@/entities/feedback'; import { ProjectGuard } from '@/entities/project'; @@ -36,35 +37,86 @@ import serverSideTranslations from '@/server-side-translations'; interface IProps { projectId: number; } - const FeedbackManagementPage: NextPageWithLayout = (props) => { const { projectId } = props; - - const router = useRouter(); - const { t } = useTranslation(); + const { data: channels, isLoading } = useAllChannels(projectId); + const [currentChannelId, setCurrentChannelId] = useQueryState( + 'channelId', + parseAsInteger.withDefault(-1), + ); useCheckAIUsageLimit(projectId); - const { data: channels, isLoading } = useAllChannels(projectId); + useEffect(() => { + if (!channels) return; + void setCurrentChannelId(channels.items[0]?.id ?? null); + }, [channels]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (channels?.meta.totalItems === 0 || currentChannelId === -1) { + return ; + } + + return ( + + ); +}; + +const EmptyChannel = (props: { projectId: number }) => { + const { projectId } = props; + const { t } = useTranslation(); const { openChannelInProgress } = useRoutingChannelCreation(projectId); - const [currentChannelId, setCurrentChannelId] = useQueryState( - 'channelId', - parseAsInteger.withDefault(-1), + return ( +
+
+ No channels +

+ {t('v2.text.no-data.channel')} +

+ +
+
); +}; - const { data: channelData, isLoading: isChannelLoading } = useOAIQuery({ +const FeedbackContainer = (props: { + projectId: number; + channelId: number; + channels: Channel[]; + onChangeChannelId: (channelId: number) => void; +}) => { + const { projectId, channelId, channels, onChangeChannelId } = props; + + const router = useRouter(); + + const { data: channelData, isLoading } = useOAIQuery({ path: '/api/admin/projects/{projectId}/channels/{channelId}', - variables: { channelId: currentChannelId, projectId }, + variables: { channelId, projectId }, queryOptions: { - enabled: currentChannelId !== -1, + enabled: channelId !== -1, }, }); - const currentChannel = useMemo(() => { - return channels?.items.find((channel) => channel.id === currentChannelId); - }, [channels, currentChannelId]); - const fields = useMemo( () => [...(channelData?.fields ?? [])].sort((a, b) => a.order - b.order), [channelData], @@ -146,12 +198,7 @@ const FeedbackManagementPage: NextPageWithLayout = (props) => { .filter((v) => !!v?.key) as TableFilterField[]; }, [channelData]); - useEffect(() => { - if (!channels || currentChannelId !== -1) return; - void setCurrentChannelId(channels.items[0]?.id ?? null); - }, [channels, currentChannelId]); - - if (isLoading || isChannelLoading) { + if (isLoading) { return (
@@ -159,28 +206,7 @@ const FeedbackManagementPage: NextPageWithLayout = (props) => { ); } - if (channels?.meta.totalItems === 0 && currentChannelId === -1) { - return ( -
-
- No channels -

- {t('v2.text.no-data.channel')} -

- -
-
- ); - } - - if (!currentChannel) { + if (!channelData) { return (
@@ -189,12 +215,12 @@ const FeedbackManagementPage: NextPageWithLayout = (props) => { Channel is invalid.

@@ -205,15 +231,16 @@ const FeedbackManagementPage: NextPageWithLayout = (props) => { return ( ); }; + FeedbackManagementPage.getLayout = (page: React.ReactElement) => { return ( @@ -227,7 +254,6 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, }) => { const projectId = parseInt(query.projectId as string); - return { props: { ...(await serverSideTranslations(locale)), diff --git a/apps/web/src/middleware.ts b/apps/web/src/proxy.ts similarity index 97% rename from apps/web/src/middleware.ts rename to apps/web/src/proxy.ts index 92e0238b3..ed3a4a9fa 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/proxy.ts @@ -19,7 +19,7 @@ import i18nConfig from 'next-i18next.config'; import { Path } from '@/shared/constants'; -export function middleware(req: NextRequest) { +export function proxy(req: NextRequest) { if (Path.isErrorPage(req.nextUrl.pathname)) return NextResponse.next(); const jwt = req.cookies.get('jwt')?.value; diff --git a/apps/web/src/shared/types/chart-payload.type.ts b/apps/web/src/shared/types/chart-payload.type.ts new file mode 100644 index 000000000..f78959cba --- /dev/null +++ b/apps/web/src/shared/types/chart-payload.type.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export type PayloadItem = { + value: number; + payload: { + fill: string; + name: string | undefined; + color: string; + }; +}; diff --git a/apps/web/src/shared/types/index.ts b/apps/web/src/shared/types/index.ts index 39154ced9..c4580ec76 100644 --- a/apps/web/src/shared/types/index.ts +++ b/apps/web/src/shared/types/index.ts @@ -23,3 +23,4 @@ export * from './form-overlay-props.type'; export * from './entity-table.type'; export * from './search-query.type'; export * from './common-form-item.type'; +export * from './chart-payload.type'; diff --git a/apps/web/src/shared/ui/charts/bar-chart.tsx b/apps/web/src/shared/ui/charts/bar-chart.tsx new file mode 100644 index 000000000..1d22c9a9f --- /dev/null +++ b/apps/web/src/shared/ui/charts/bar-chart.tsx @@ -0,0 +1,110 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { + Bar, + CartesianGrid, + Cell, + BarChart as RechartBarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import type { PayloadItem } from '@/shared/types'; + +interface Data { + name: string; + value: number; + color: string; +} + +interface IProps { + height?: number; + data: Data[]; + onClick?: (name?: string) => void; +} + +const BarChart: React.FC = (props) => { + const { data, height, onClick } = props; + return ( + + onClick?.(nextState.activeLabel)} + > + + ( +
+ {payload.map( + ({ value, payload: itemPayload }: PayloadItem, i) => ( +
+
+
+

+ {itemPayload.name} +

+
+

+ {typeof value === 'number' ? + value.toLocaleString() + : value} +

+
+ ), + )} +
+ )} + /> + + v.toLocaleString()} + className="text-neutral-tertiary text-small-normal" + tickLine={false} + axisLine={false} + /> + + {data.map((entry, index) => ( + + ))} + + + + ); +}; + +export default BarChart; diff --git a/apps/web/src/shared/ui/charts/chart-card.tsx b/apps/web/src/shared/ui/charts/chart-card.tsx index 3a851913f..6263073dc 100644 --- a/apps/web/src/shared/ui/charts/chart-card.tsx +++ b/apps/web/src/shared/ui/charts/chart-card.tsx @@ -15,19 +15,16 @@ */ import DescriptionTooltip from '../description-tooltip'; -import Legend from './legend'; interface IProps extends React.PropsWithChildren { title: string; description?: string; - dataKeys?: { name: string; color: string }[]; - showLegend?: boolean; - filterContent?: React.ReactNode; + extra?: React.ReactNode; } const ChartCard: React.FC = (props) => { - const { children, description, title, dataKeys, filterContent, showLegend } = - props; + const { children, description, title, extra } = props; + return (
@@ -37,10 +34,7 @@ const ChartCard: React.FC = (props) => { )}
-
- {showLegend && } - {filterContent} -
+
{extra}
{children}
diff --git a/apps/web/src/shared/ui/charts/index.ts b/apps/web/src/shared/ui/charts/index.ts index 53e79d503..c86d07848 100644 --- a/apps/web/src/shared/ui/charts/index.ts +++ b/apps/web/src/shared/ui/charts/index.ts @@ -13,6 +13,9 @@ * License for the specific language governing permissions and limitations * under the License. */ -export { default as SimpleBarChart } from './simple-bar-chart'; -export { default as SimpleLineChart } from './simple-line-chart'; -export { default as SimplePieChart } from './simple-pie-chart'; + +export { default as BarChart } from './bar-chart'; +export { default as LineChart } from './line-chart'; +export { default as PieChart } from './pie-chart'; +export { default as ChartCard } from './chart-card'; +export { default as Legend } from './legend'; diff --git a/apps/web/src/shared/ui/charts/line-chart.tsx b/apps/web/src/shared/ui/charts/line-chart.tsx new file mode 100644 index 000000000..92238d5fc --- /dev/null +++ b/apps/web/src/shared/ui/charts/line-chart.tsx @@ -0,0 +1,165 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { useMemo } from 'react'; +import dayjs from 'dayjs'; +import { useTranslation } from 'next-i18next'; +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import type { TooltipContentProps } from 'recharts'; + +interface IProps { + height?: number; + data: unknown[]; + dataKeys: { color: string; name: string }[]; +} + +const LineChart: React.FC = (props) => { + const { height, data, dataKeys } = props; + + return ( + + + + + content={(props) => } + formatter={(value) => value.toLocaleString()} + /> + + v.toLocaleString()} + className="text-neutral-tertiary text-small-normal" + tickSize={15} + tickLine={false} + min={0} + axisLine={false} + /> + + {dataKeys.map(({ color }, index) => ( + + + + + ))} + + {dataKeys.map(({ color, name }, index) => ( + + ))} + + + ); +}; + +interface ICustomTooltipProps extends TooltipContentProps {} + +const CustomTooltip: React.FC = (props) => { + const { active, payload, label } = props; + const { t } = useTranslation(); + const days = useMemo(() => { + if (!label || typeof label !== 'string' || !label.includes('-')) { + return null; + } + const [start, end] = label.split(' - '); + const dates = dayjs(end).diff(dayjs(start), 'day') + 1; + return dates > 0 ? dates : 365 + dates; + }, [label]); + + if (!active) return null; + return ( +
+

+ {label} + {days && ( + + ({t('text.days', { days })}) + + )} +

+
+ {payload.map((item, i) => { + const { + color, + name, + value, + payload: itemPayload, + } = item as { + color: string; + name: string; + value: unknown; + payload: unknown; + }; + return ( +
+
+
+

+ {name || (itemPayload as { date: string | undefined }).date} +

+
+

+ {typeof value === 'number' ? + value.toLocaleString() + : String(value)} +

+
+ ); + })} +
+
+ ); +}; + +export default LineChart; diff --git a/apps/web/src/shared/ui/charts/pie-chart.tsx b/apps/web/src/shared/ui/charts/pie-chart.tsx new file mode 100644 index 000000000..218e60a42 --- /dev/null +++ b/apps/web/src/shared/ui/charts/pie-chart.tsx @@ -0,0 +1,117 @@ +/** + * Copyright 2025 LY Corporation + * + * LY Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { useMemo } from 'react'; +import { + Cell, + Label, + Pie, + PieChart as RechartPieChart, + ResponsiveContainer, + Tooltip, +} from 'recharts'; + +import type { PayloadItem } from '@/shared'; + +interface Data { + name: string; + value: number; + color?: string; + [key: string]: unknown; +} + +interface IProps { + height?: number; + data: Data[]; + onClick?: (name?: string) => void; +} + +const PieChart: React.FC = (props) => { + const { data, height, onClick } = props; + + const total = useMemo( + () => data.reduce((acc, item) => acc + item.value, 0), + [data], + ); + + return ( + + + + {data.map((entry, index) => ( + onClick?.(entry.name)} + className="focus:outline-none" + /> + ))} + + ( +
+ {payload.map(({ payload, value }: PayloadItem, i) => ( +
+
+
+

+ {payload.name} +

+
+

{value.toLocaleString()}

+
+ ))} +
+ )} + /> + + + ); +}; + +export default PieChart; diff --git a/apps/web/src/shared/ui/charts/simple-bar-chart.tsx b/apps/web/src/shared/ui/charts/simple-bar-chart.tsx deleted file mode 100644 index 7ff2c4b82..000000000 --- a/apps/web/src/shared/ui/charts/simple-bar-chart.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright 2025 LY Corporation - * - * LY Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { - Bar, - BarChart, - CartesianGrid, - Cell, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; - -import ChartCard from './chart-card'; - -interface Data { - name: string; - value: number; - color: string; -} - -interface IProps { - title: string; - description: string; - height?: number; - data: Data[]; - showLegend?: boolean; - onClick?: (data?: Data) => void; - filterContent?: React.ReactNode; -} - -const SimpleBarChart: React.FC = (props) => { - const { - data, - title, - description, - height, - showLegend, - onClick, - filterContent, - } = props; - return ( - - - - onClick?.(e.activePayload?.[0]?.payload) - } - > - - value.toLocaleString()} - cursor={{ fill: 'var(--bg-neutral-tertiary)' }} - content={({ payload }) => ( -
- {payload?.map(({ value, payload }, i) => ( -
-
-
-

- {(payload as { name: string | undefined }).name} -

-
-

{value?.toLocaleString()}

-
- ))} -
- )} - /> - - v.toLocaleString()} - className="text-neutral-tertiary text-small-normal" - tickLine={false} - axisLine={false} - /> - - {data.map((entry, index) => ( - - ))} - - - - - ); -}; - -export default SimpleBarChart; diff --git a/apps/web/src/shared/ui/charts/simple-line-chart.tsx b/apps/web/src/shared/ui/charts/simple-line-chart.tsx deleted file mode 100644 index 57e8dc1e7..000000000 --- a/apps/web/src/shared/ui/charts/simple-line-chart.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright 2025 LY Corporation - * - * LY Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { useMemo } from 'react'; -import dayjs from 'dayjs'; -import { useTranslation } from 'next-i18next'; -import { - Area, - AreaChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; -import type { TooltipProps } from 'recharts'; -import type { - NameType, - ValueType, -} from 'recharts/types/component/DefaultTooltipContent'; - -import ChartCard from './chart-card'; - -interface IProps { - title: string; - description?: string; - height?: number; - data: unknown[]; - dataKeys: { color: string; name: string }[]; - showLegend?: boolean; - filterContent?: React.ReactNode; - noLabel?: boolean; -} - -const SimpleLineChart: React.FC = (props) => { - const { - title, - description, - height, - data, - dataKeys, - showLegend, - filterContent, - noLabel = false, - } = props; - - return ( - - - - - } - formatter={(value) => value.toLocaleString()} - /> - - v.toLocaleString()} - className="text-neutral-tertiary text-small-normal" - tickSize={15} - tickLine={false} - min={0} - axisLine={false} - /> - - {dataKeys.map(({ color }, index) => ( - - - - - ))} - - {dataKeys.map(({ color, name }, index) => ( - - ))} - - - - ); -}; - -interface ICustomTooltipProps - extends Omit, 'label'> { - noLabel: boolean; - label?: string; -} - -const CustomTooltip: React.FC = (props) => { - const { active, payload, label, noLabel } = props; - const { t } = useTranslation(); - const days = useMemo(() => { - if (!label || typeof label !== 'string' || !label.includes('-')) { - return null; - } - const [start, end] = label.split(' - '); - const dates = dayjs(end).diff(dayjs(start), 'day') + 1; - return dates > 0 ? dates : 365 + dates; - }, [label]); - - if (!active || !payload) return null; - return ( -
-

- {label} - {days && ( - - ({t('text.days', { days })}) - - )} -

-
- {payload.map(({ color, name, value, payload }, i) => ( -
- {!noLabel && ( -
-
-

- {name ?? (payload as { date: string | undefined }).date} -

-
- )} -

{value?.toLocaleString()}

-
- ))} -
-
- ); -}; - -export default SimpleLineChart; diff --git a/apps/web/src/shared/ui/charts/simple-pie-chart.tsx b/apps/web/src/shared/ui/charts/simple-pie-chart.tsx deleted file mode 100644 index d871642ff..000000000 --- a/apps/web/src/shared/ui/charts/simple-pie-chart.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright 2025 LY Corporation - * - * LY Corporation licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { useMemo } from 'react'; -import { - Cell, - Label, - Legend, - Pie, - PieChart, - ResponsiveContainer, - Tooltip, -} from 'recharts'; -import type { ContentType } from 'recharts/types/component/DefaultLegendContent'; - -import ChartCard from './chart-card'; - -interface Data { - name: string; - value: number; - color?: string; -} - -interface IProps { - title: string; - description: string; - height?: number; - data: Data[]; - showLegend?: boolean; - onClick?: (data?: Data) => void; - filterContent?: React.ReactNode; -} - -const SimplePieChart: React.FC = (props) => { - const { - data, - title, - description, - height, - showLegend, - onClick, - filterContent, - } = props; - const total = useMemo( - () => data.reduce((acc, item) => acc + item.value, 0), - [data], - ); - return ( - - - - - {data.map((entry, index) => ( - onClick?.(entry)} - className="focus:outline-none" - /> - ))} - - - value.toLocaleString()} - cursor={{ fill: 'var(--bg-neutral-tertiary)' }} - content={({ payload }) => { - return ( -
- {payload?.map(({ value, payload }, i) => ( -
-
-
-

- {(payload as { name: string | undefined }).name} -

-
-

{value?.toLocaleString()}

-
- ))} -
- ); - }} - /> - - - - ); -}; - -const CustomLegend: ContentType = ({ payload }) => { - return ( -
- - - - - - - - - {payload?.map((entry, index) => ( - - - - - ))} - -
- Status - - Issue Count -
-
- - {entry.value} -
-
- {entry.payload?.value} -
-
- ); -}; - -export default SimplePieChart; diff --git a/apps/web/src/shared/ui/date-range-picker.tsx b/apps/web/src/shared/ui/date-range-picker.tsx index 02cd2d8ad..8eb701bfe 100644 --- a/apps/web/src/shared/ui/date-range-picker.tsx +++ b/apps/web/src/shared/ui/date-range-picker.tsx @@ -30,6 +30,9 @@ import { PopoverContent, PopoverTrigger, toast, + Tooltip, + TooltipContent, + TooltipTrigger, } from '@ufb/react'; import type { DateRangeType } from '../types/date-range.type'; @@ -58,6 +61,7 @@ interface IProps { }[]; children?: React.ReactNode; numberOfMonths?: number; + tooltipContent?: string; } const DateRangePicker: React.FC = (props) => { @@ -71,6 +75,7 @@ const DateRangePicker: React.FC = (props) => { children, numberOfMonths = 2, allowEntirePeriod = true, + tooltipContent, } = props; const { t, i18n } = useTranslation(); @@ -205,15 +210,11 @@ const DateRangePicker: React.FC = (props) => { toast.error(t('text.date.date-range-over-max-days', { maxDays })); return; } - onChange( - ( - currentValue && - currentValue.startDate === null && - currentValue.endDate === null - ) ? - null - : currentValue, - ); + const shouldSetNull = + currentValue !== null && + currentValue.startDate === null && + currentValue.endDate === null; + onChange(shouldSetNull ? null : currentValue); setIsOpen(false); }; @@ -318,20 +319,25 @@ const DateRangePicker: React.FC = (props) => { return ( - - {children ? - - : - } - + + + + {children ? + + : + } + + + {tooltipContent && {tooltipContent}} +
{numberOfMonths === 2 && ( diff --git a/apps/web/src/shared/ui/index.ts b/apps/web/src/shared/ui/index.ts index 6e3e57318..747dd31dd 100644 --- a/apps/web/src/shared/ui/index.ts +++ b/apps/web/src/shared/ui/index.ts @@ -48,10 +48,10 @@ export { default as LanguageSelectBox } from './language-select-box.ui'; export { default as AnonymousTemplate } from './anonymous-template.ui'; export { default as ThemeSelectBox } from './theme-select-box.ui'; export { default as SheetDetailTable } from './sheet-detail-table.ui'; -export * from './table-filter-popover'; export { default as NoProjectDialogInProjectCreation } from './no-project-dialog-in-project-creation.ui'; export { default as InfiniteScrollArea } from './infinite-scroll-area.ui'; export { default as FeedbackImage } from './feedback-image'; +export * from './table-filter-popover'; export * from './card.ui'; export * from './slider.ui'; diff --git a/apps/web/src/shared/ui/tables/table-pagination.tsx b/apps/web/src/shared/ui/tables/table-pagination.tsx index b5fd5bfdf..37e45e69b 100644 --- a/apps/web/src/shared/ui/tables/table-pagination.tsx +++ b/apps/web/src/shared/ui/tables/table-pagination.tsx @@ -63,7 +63,9 @@ const TablePagination = (props: IProps) => { {PAGE_SIZES.map((pageSize) => ( table.setPageSize(pageSize)} + onClick={() => + table.setPagination({ pageIndex: 0, pageSize }) + } > {pageSize} diff --git a/apps/web/src/widgets/issue-table/ui/issue-table.ui.tsx b/apps/web/src/widgets/issue-table/ui/issue-table.ui.tsx index c8d6a7c94..9076c08ca 100644 --- a/apps/web/src/widgets/issue-table/ui/issue-table.ui.tsx +++ b/apps/web/src/widgets/issue-table/ui/issue-table.ui.tsx @@ -18,15 +18,7 @@ import { useOverlay } from '@toss/use-overlay'; import { useTranslation } from 'next-i18next'; import { parseAsString, useQueryState } from 'nuqs'; -import { - Button, - Icon, - ToggleGroup, - ToggleGroupItem, - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@ufb/react'; +import { Button, Icon, ToggleGroup, ToggleGroupItem } from '@ufb/react'; import type { TableFilterField } from '@/shared'; import { @@ -151,18 +143,12 @@ const IssueTable: React.FC = ({ projectId }) => { {t('main.feedback.issue-cell.create-issue')}
- - - - - - {t('tooltip.issue-date-picker-button')} - - + =22.19.0", - "pnpm": "^10.15.1" + "node": ">=24.13.0", + "pnpm": "^10.28.1" }, "scripts": { "build": "turbo run build", + "build:api": "turbo run build -F api...", + "build:web": "turbo run build -F web...", "clean": "git clean -xdf node_modules .turbo", "clean:workspaces": "turbo run clean", "dev": "turbo watch dev --continue", @@ -27,7 +29,7 @@ "devDependencies": { "@ufb/prettier-config": "workspace:*", "prettier": "catalog:", - "turbo": "^2.5.6", + "turbo": "^2.7.5", "typescript": "catalog:" }, "prettier": "@ufb/prettier-config", diff --git a/packages/ufb-react/package.json b/packages/ufb-react/package.json index ddb5a7640..e8e590c4a 100644 --- a/packages/ufb-react/package.json +++ b/packages/ufb-react/package.json @@ -17,41 +17,41 @@ "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-menubar": "^1.1.16", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", - "@remixicon/react": "^4.6.0", + "@remixicon/react": "^4.8.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "lucide-react": "^0.543.0", + "lucide-react": "^0.562.0", "react-day-picker": "8.10.1", - "react-hook-form": "^7.62.0", + "react-hook-form": "^7.71.1", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.4.0" }, "devDependencies": { - "@types/react": "^19.1.12", - "@types/react-dom": "^19.1.9", + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", "@ufb/eslint-config": "workspace:^", "@ufb/prettier-config": "workspace:^", "@ufb/tailwindcss": "workspace:^", "@ufb/tsconfig": "workspace:^", "eslint": "catalog:", "prettier": "catalog:", - "react": "^19.1.1", + "react": "^19.2.3", "tailwindcss": "catalog:", "typescript": "catalog:" }, diff --git a/packages/ufb-react/src/components/button.tsx b/packages/ufb-react/src/components/button.tsx index 523f78726..cfb7521e6 100644 --- a/packages/ufb-react/src/components/button.tsx +++ b/packages/ufb-react/src/components/button.tsx @@ -116,7 +116,7 @@ const Button = React.forwardRef( ((node as HTMLElement).nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName.toLowerCase() === 'svg') || ((node as HTMLElement).nodeType === Node.TEXT_NODE && - !(node as HTMLElement).textContent?.trim()), + !((node as HTMLElement).textContent || '').trim()), ); if (isSvgOnly) { diff --git a/packages/ufb-shared/package.json b/packages/ufb-shared/package.json index b1035cbc1..587d96eb0 100644 --- a/packages/ufb-shared/package.json +++ b/packages/ufb-shared/package.json @@ -21,8 +21,8 @@ "@ufb/tsconfig": "workspace:*", "eslint": "catalog:", "prettier": "catalog:", - "react": "^19.1.1", - "tsup": "^8.5.0", + "react": "^19.2.3", + "tsup": "^8.5.1", "typescript": "catalog:" } } diff --git a/packages/ufb-shared/src/error-code.enum.ts b/packages/ufb-shared/src/error-code.enum.ts index 6d9a26be1..996f9bbc8 100644 --- a/packages/ufb-shared/src/error-code.enum.ts +++ b/packages/ufb-shared/src/error-code.enum.ts @@ -102,6 +102,7 @@ const Opensearch = { const Webhook = { WebhookAlreadyExists: 'WebhookAlreadyExists', + WebhookNotFound: 'WebhookNotFound', }; export const ErrorCode = { diff --git a/packages/ufb-tailwindcss/package.json b/packages/ufb-tailwindcss/package.json index bde4fc6ab..5b4c611ef 100644 --- a/packages/ufb-tailwindcss/package.json +++ b/packages/ufb-tailwindcss/package.json @@ -19,13 +19,13 @@ "@ufb/eslint-config": "workspace:*", "@ufb/prettier-config": "workspace:*", "@ufb/tsconfig": "workspace:^", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.23", "eslint": "catalog:", - "glob": "^11.0.3", + "glob": "^13.0.0", "postcss": "^8.5.6", "postcss-import": "^16.1.1", - "postcss-js": "^4.0.1", - "postcss-nesting": "^13.0.2", + "postcss-js": "^5.0.3", + "postcss-nesting": "^14.0.0", "prettier": "catalog:", "tailwindcss": "catalog:" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72ef23db3..458d99af4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,17 +7,17 @@ settings: catalogs: default: eslint: - specifier: ^9.35.0 - version: 9.35.0 + specifier: ^9.39.2 + version: 9.39.2 prettier: specifier: ^3.5.3 - version: 3.5.3 + version: 3.6.2 tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 + specifier: ^3.4.19 + version: 3.4.19 typescript: - specifier: ^5.8.3 - version: 5.8.3 + specifier: ^5.9.3 + version: 5.9.3 importers: @@ -28,70 +28,70 @@ importers: version: link:tooling/prettier prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 turbo: - specifier: ^2.5.6 - version: 2.5.6 + specifier: ^2.7.5 + version: 2.7.5 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 apps/api: dependencies: '@aws-sdk/client-s3': - specifier: ^3.884.0 - version: 3.884.0 + specifier: ^3.971.0 + version: 3.971.0 '@aws-sdk/s3-request-presigner': - specifier: ^3.884.0 - version: 3.884.0 + specifier: ^3.971.0 + version: 3.971.0 '@fastify/multipart': - specifier: ^9.2.1 - version: 9.2.1 + specifier: ^9.4.0 + version: 9.4.0 '@fastify/static': - specifier: ^8.2.0 - version: 8.2.0 + specifier: ^9.0.0 + version: 9.0.0 '@nestjs-modules/mailer': specifier: ^2.0.2 - version: 2.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(nodemailer@7.0.6) + version: 2.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(nodemailer@7.0.12) '@nestjs/axios': specifier: ^4.0.1 - version: 4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.11.0)(rxjs@7.8.2) + version: 4.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) '@nestjs/common': - specifier: ^11.1.6 - version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.12 + version: 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^11.1.6 - version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.12 + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + version: 3.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/jwt': - specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/passport': specifier: ^11.0.5 - version: 11.0.5(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + version: 11.0.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-express': - specifier: ^11.1.6 - version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + specifier: ^11.1.12 + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/platform-fastify': - specifier: ^11.1.6 - version: 11.1.6(@fastify/static@8.2.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + specifier: ^11.1.12 + version: 11.1.12(@fastify/static@9.0.0)(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/schedule': - specifier: ^6.0.0 - version: 6.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + specifier: ^6.0.1 + version: 6.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/swagger': - specifier: ^11.2.0 - version: 11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + specifier: ^11.2.5 + version: 11.2.5(@fastify/static@9.0.0)(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: ^11.0.0 - version: 11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.11.0)(rxjs@7.8.2))(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))) + version: 11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))) + version: 11.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))) '@opensearch-project/opensearch': specifier: ^3.5.1 version: 3.5.1 @@ -106,10 +106,10 @@ importers: version: link:../../packages/ufb-shared '@willsoto/nestjs-prometheus': specifier: ^6.0.2 - version: 6.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) + version: 6.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.13.2 + version: 1.13.2 bcrypt: specifier: ^6.0.0 version: 6.0.0 @@ -117,14 +117,14 @@ importers: specifier: ^0.5.1 version: 0.5.1 class-validator: - specifier: ^0.14.2 - version: 0.14.2 + specifier: ^0.14.3 + version: 0.14.3 cron: - specifier: ^4.3.0 - version: 4.3.0 + specifier: ^4.3.3 + version: 4.3.3 dotenv: - specifier: ^17.2.2 - version: 17.2.2 + specifier: ^17.2.3 + version: 17.2.3 exceljs: specifier: ^4.4.0 version: 4.4.0 @@ -132,11 +132,11 @@ importers: specifier: ^5.0.5 version: 5.0.5 fastify: - specifier: ^5.4.0 - version: 5.4.0 + specifier: ^5.7.1 + version: 5.7.1 joi: - specifier: ^18.0.1 - version: 18.0.1 + specifier: ^18.0.2 + version: 18.0.2 luxon: specifier: ^3.7.2 version: 3.7.2 @@ -144,20 +144,20 @@ importers: specifier: ^1.12.1 version: 1.12.1 mysql2: - specifier: ^3.14.5 - version: 3.14.5 + specifier: ^3.16.1 + version: 3.16.1 nestjs-cls: - specifier: ^6.0.1 - version: 6.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^6.2.0 + version: 6.2.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-pino: - specifier: ^4.4.0 - version: 4.4.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@10.5.0)(pino@9.9.4)(rxjs@7.8.2) + specifier: ^4.5.0 + version: 4.5.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.2.0)(rxjs@7.8.2) nestjs-typeorm-paginate: specifier: ^4.1.0 - version: 4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))) + version: 4.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))) nodemailer: - specifier: ^7.0.6 - version: 7.0.6 + specifier: ^7.0.12 + version: 7.0.12 passport: specifier: ^0.7.0 version: 0.7.0 @@ -171,11 +171,11 @@ importers: specifier: ^1.0.0 version: 1.0.0 pino-http: - specifier: ^10.5.0 - version: 10.5.0 + specifier: ^11.0.0 + version: 11.0.0 pino-pretty: - specifier: ^13.1.1 - version: 13.1.1 + specifier: ^13.1.3 + version: 13.1.3 prom-client: specifier: ^15.1.3 version: 15.1.3 @@ -189,48 +189,48 @@ importers: specifier: ^0.5.21 version: 0.5.21 typeorm: - specifier: ^0.3.26 - version: 0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + specifier: ^0.3.28 + version: 0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) typeorm-naming-strategies: specifier: ^4.1.0 - version: 4.1.0(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))) + version: 4.1.0(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))) typeorm-transactional: specifier: ^0.5.0 - version: 0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))) + version: 0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))) uuid: - specifier: ^11.1.0 - version: 11.1.0 + specifier: ^13.0.0 + version: 13.0.0 devDependencies: '@faker-js/faker': - specifier: ^9.9.0 - version: 9.9.0 + specifier: ^10.2.0 + version: 10.2.0 '@nestjs/cli': - specifier: ^11.0.10 - version: 11.0.10(@swc/cli@0.7.8(@swc/core@1.13.5(@swc/helpers@0.5.17))(chokidar@4.0.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1) + specifier: ^11.0.16 + version: 11.0.16(@swc/cli@0.7.10(@swc/core@1.13.5(@swc/helpers@0.5.18))(chokidar@4.0.3))(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8) '@nestjs/schematics': - specifier: ^11.0.7 - version: 11.0.7(chokidar@4.0.3)(typescript@5.8.3) + specifier: ^11.0.9 + version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': - specifier: ^11.1.6 - version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6) + specifier: ^11.1.12 + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12) '@swc-node/jest': specifier: ^1.9.1 - version: 1.9.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.24)(typescript@5.8.3) + version: 1.9.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3) '@swc/cli': - specifier: 0.7.8 - version: 0.7.8(@swc/core@1.13.5(@swc/helpers@0.5.17))(chokidar@4.0.3) + specifier: 0.7.10 + version: 0.7.10(@swc/core@1.13.5(@swc/helpers@0.5.18))(chokidar@4.0.3) '@swc/core': specifier: 1.13.5 - version: 1.13.5(@swc/helpers@0.5.17) + version: 1.13.5(@swc/helpers@0.5.18) '@swc/helpers': - specifier: ^0.5.17 - version: 0.5.17 + specifier: ^0.5.18 + version: 0.5.18 '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 '@types/express': - specifier: ^5.0.3 - version: 5.0.3 + specifier: ^5.0.6 + version: 5.0.6 '@types/jest': specifier: ^30.0.0 version: 30.0.0 @@ -238,17 +238,17 @@ importers: specifier: ^3.7.1 version: 3.7.1 '@types/node': - specifier: 22.18.1 - version: 22.18.1 + specifier: 24.10.8 + version: 24.10.8 '@types/nodemailer': - specifier: ^7.0.1 - version: 7.0.1 + specifier: ^7.0.5 + version: 7.0.5 '@types/supertest': specifier: ^6.0.3 version: 6.0.3 '@typescript-eslint/parser': - specifier: ^8.43.0 - version: 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.46.0 + version: 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@ufb/eslint-config': specifier: workspace:* version: link:../../tooling/eslint @@ -260,34 +260,34 @@ importers: version: link:../../tooling/typescript eslint: specifier: 'catalog:' - version: 9.35.0(jiti@2.4.2) + version: 9.39.2(jiti@1.21.7) jest: - specifier: ^30.1.3 - version: 30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + specifier: ^30.2.0 + version: 30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) mockdate: specifier: ^3.0.5 version: 3.0.5 prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 supertest: - specifier: ^7.1.4 - version: 7.1.4 + specifier: ^7.2.2 + version: 7.2.2 ts-jest: - specifier: ^29.4.1 - version: 29.4.1(@babel/core@7.28.4)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.4))(jest-util@30.0.5)(jest@30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)))(typescript@5.8.3) + specifier: ^29.4.6 + version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.4 - version: 9.5.4(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))) + version: 9.5.4(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18))) ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3) + version: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 apps/cli: dependencies: @@ -298,12 +298,24 @@ importers: specifier: ^1.0.2 version: 1.0.2 commander: - specifier: ^14.0.0 - version: 14.0.0 + specifier: ^14.0.2 + version: 14.0.2 js-toml: specifier: ^1.0.2 version: 1.0.2 + toml: + specifier: ^3.0.0 + version: 3.0.0 + yaml: + specifier: ^2.8.2 + version: 2.8.2 + zod: + specifier: ^4.3.5 + version: 4.3.5 devDependencies: + '@types/node': + specifier: 24.10.8 + version: 24.10.8 '@ufb/eslint-config': specifier: workspace:* version: link:../../tooling/eslint @@ -315,61 +327,125 @@ importers: version: link:../../tooling/typescript ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3) + version: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3) + typescript: + specifier: 'catalog:' + version: 5.9.3 + + apps/docs: + dependencies: + '@docusaurus/core': + specifier: 3.9.2 + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-google-gtag': + specifier: ^3.9.2 + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/preset-classic': + specifier: 3.9.2 + version: 3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3) + '@mdx-js/react': + specifier: ^3.1.1 + version: 3.1.1(@types/react@19.2.8)(react@19.2.3) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + prism-react-renderer: + specifier: ^2.4.1 + version: 2.4.1(react@19.2.3) + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@docusaurus/module-type-aliases': + specifier: 3.9.2 + version: 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/tsconfig': + specifier: 3.9.2 + version: 3.9.2 + '@docusaurus/types': + specifier: 3.9.2 + version: 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ufb/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@ufb/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@ufb/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + eslint: + specifier: 'catalog:' + version: 9.39.2(jiti@1.21.7) + prettier: + specifier: 'catalog:' + version: 3.6.2 + typescript: + specifier: 'catalog:' + version: 5.9.3 apps/e2e: devDependencies: + '@opensearch-project/opensearch': + specifier: ^3.5.1 + version: 3.5.1 '@playwright/test': - specifier: ^1.55.0 - version: 1.55.0 + specifier: ^1.57.0 + version: 1.57.0 axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.13.2 + version: 1.13.2 mysql2: - specifier: ^3.14.5 - version: 3.14.5 + specifier: ^3.16.1 + version: 3.16.1 apps/web: dependencies: '@dnd-kit/core': specifier: ^6.3.1 - version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@dnd-kit/modifiers': specifier: ^9.0.0 - version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@dnd-kit/sortable': specifier: ^10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@dnd-kit/utilities': specifier: ^3.2.2 - version: 3.2.2(react@19.1.1) + version: 3.2.2(react@19.2.3) '@faker-js/faker': - specifier: ^9.9.0 - version: 9.9.0 + specifier: ^10.2.0 + version: 10.2.0 '@hookform/resolvers': - specifier: ^5.2.1 - version: 5.2.1(react-hook-form@7.62.0(react@19.1.1)) + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.71.1(react@19.2.3)) '@number-flow/react': specifier: ^0.5.10 - version: 0.5.10(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 0.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-slider': specifier: ^1.3.6 - version: 1.3.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@t3-oss/env-nextjs': - specifier: ^0.13.8 - version: 0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@4.1.5) + specifier: ^0.13.10 + version: 0.13.10(typescript@5.9.3)(zod@4.3.5) '@tanstack/react-query': - specifier: ^5.87.1 - version: 5.87.1(react@19.1.1) + specifier: ^5.90.19 + version: 5.90.19(react@19.2.3) '@tanstack/react-query-devtools': - specifier: ^5.87.1 - version: 5.87.1(@tanstack/react-query@5.87.1(react@19.1.1))(react@19.1.1) + specifier: ^5.91.2 + version: 5.91.2(@tanstack/react-query@5.90.19(react@19.2.3))(react@19.2.3) '@tanstack/react-table': specifier: ^8.21.3 - version: 8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@toss/use-overlay': specifier: ^1.4.2 - version: 1.4.2(react@19.1.1) + version: 1.4.2(react@19.2.3) '@ufb/react': specifier: workspace:* version: link:../../packages/ufb-react @@ -380,11 +456,11 @@ importers: specifier: workspace:* version: link:../../packages/ufb-tailwindcss axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.13.2 + version: 1.13.2 axios-auth-refresh: specifier: ^3.3.6 - version: 3.3.6(axios@1.11.0) + version: 3.3.6(axios@1.13.2) classnames: specifier: ^2.5.1 version: 2.5.1 @@ -392,8 +468,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 cookies-next: - specifier: ^6.1.0 - version: 6.1.0(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + specifier: ^6.1.1 + version: 6.1.1(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) countries-and-timezones: specifier: ^3.8.0 version: 3.8.0 @@ -401,17 +477,17 @@ importers: specifier: ^4.1.0 version: 4.1.0 dayjs: - specifier: ^1.11.18 - version: 1.11.18 + specifier: ^1.11.19 + version: 1.11.19 framer-motion: - specifier: ^12.23.12 - version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^12.27.1 + version: 12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) i18next: - specifier: ^25.5.2 - version: 25.5.2(typescript@5.8.3) + specifier: ^25.7.4 + version: 25.7.4(typescript@5.9.3) immer: - specifier: ^10.1.3 - version: 10.1.3 + specifier: ^11.1.3 + version: 11.1.3 jwt-decode: specifier: ^4.0.0 version: 4.0.0 @@ -420,101 +496,101 @@ importers: version: 0.2.1 linkify-react: specifier: ^4.3.2 - version: 4.3.2(linkifyjs@4.2.0)(react@19.1.1) + version: 4.3.2(linkifyjs@4.3.2)(react@19.2.3) next: - specifier: ^15.4.7 - version: 15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^16.1.3 + version: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-i18next: - specifier: ^15.4.2 - version: 15.4.2(i18next@25.5.2(typescript@5.8.3))(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-i18next@15.7.3(i18next@25.5.2(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3))(react@19.1.1) + specifier: ^15.4.3 + version: 15.4.3(@types/react@19.2.8)(i18next@25.7.4(typescript@5.9.3))(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-i18next@16.5.3(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react@19.2.3) next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nuqs: specifier: 2.4.3 - version: 2.4.3(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 2.4.3(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) pino: - specifier: ^9.9.4 - version: 9.9.4 + specifier: ^10.2.0 + version: 10.2.0 react: - specifier: ^19.1.1 - version: 19.1.1 + specifier: ^19.2.3 + version: 19.2.3 react-content-loader: specifier: ^7.1.1 - version: 7.1.1(react@19.1.1) + version: 7.1.1(react@19.2.3) react-dom: - specifier: ^19.1.1 - version: 19.1.1(react@19.1.1) + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) react-hook-form: - specifier: ^7.62.0 - version: 7.62.0(react@19.1.1) + specifier: ^7.71.1 + version: 7.71.1(react@19.2.3) react-i18next: - specifier: ^15.7.3 - version: 15.7.3(i18next@25.5.2(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3) + specifier: ^16.5.3 + version: 16.5.3(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) react-select: specifier: ^5.10.2 - version: 5.10.2(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 5.10.2(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-use: specifier: ^17.6.0 - version: 17.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 17.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) recharts: - specifier: ^2.15.4 - version: 2.15.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^3.5.0 + version: 3.5.0(@types/react@19.2.8)(eslint@9.39.2(jiti@1.21.7))(react-dom@19.2.3(react@19.2.3))(react-is@18.3.1)(react@19.2.3)(redux@5.0.1) sharp: - specifier: ^0.34.3 - version: 0.34.3 + specifier: ^0.34.5 + version: 0.34.5 swiper: - specifier: ^11.2.10 - version: 11.2.10 + specifier: ^12.0.3 + version: 12.0.3 tailwind-merge: - specifier: ^3.3.1 - version: 3.3.1 + specifier: ^3.4.0 + version: 3.4.0 tailwind-scrollbar-hide: specifier: ^4.0.0 - version: 4.0.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))) + version: 4.0.0(tailwindcss@3.4.19(yaml@2.8.2)) zod: - specifier: ^4.1.5 - version: 4.1.5 + specifier: ^4.3.5 + version: 4.3.5 zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@19.1.12)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)) + specifier: ^5.0.10 + version: 5.0.10(@types/react@19.2.8)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: '@babel/core': - specifier: ^7.28.4 - version: 7.28.4 + specifier: ^7.28.6 + version: 7.28.6 '@rollup/plugin-commonjs': - specifier: ^28.0.6 - version: 28.0.6(rollup@4.34.8) + specifier: ^29.0.0 + version: 29.0.0(rollup@4.52.5) '@svgr/webpack': specifier: ^8.1.0 - version: 8.1.0(typescript@5.8.3) + version: 8.1.0(typescript@5.9.3) '@swc/core': specifier: 1.13.5 - version: 1.13.5(@swc/helpers@0.5.17) + version: 1.13.5(@swc/helpers@0.5.18) '@swc/jest': specifier: ^0.2.39 - version: 0.2.39(@swc/core@1.13.5(@swc/helpers@0.5.17)) + version: 0.2.39(@swc/core@1.13.5(@swc/helpers@0.5.18)) '@testing-library/jest-dom': - specifier: ^6.8.0 - version: 6.8.0 + specifier: ^6.9.1 + version: 6.9.1 '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.3.2)(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@testing-library/user-event': specifier: ^14.6.1 - version: 14.6.1(@testing-library/dom@10.3.2) + version: 14.6.1(@testing-library/dom@10.4.1) '@types/jest': specifier: ^30.0.0 version: 30.0.0 '@types/node': - specifier: 22.18.1 - version: 22.18.1 + specifier: 24.10.8 + version: 24.10.8 '@types/react': - specifier: ^19.1.12 - version: 19.1.12 + specifier: ^19.2.8 + version: 19.2.8 '@types/react-dom': - specifier: ^19.1.9 - version: 19.1.9(@types/react@19.1.12) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.8) '@ufb/eslint-config': specifier: workspace:* version: link:../../tooling/eslint @@ -525,104 +601,104 @@ importers: specifier: workspace:* version: link:../../tooling/typescript autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.6) eslint: specifier: 'catalog:' - version: 9.35.0(jiti@1.21.7) + version: 9.39.2(jiti@1.21.7) jest: - specifier: ^30.1.3 - version: 30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + specifier: ^30.2.0 + version: 30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) jest-environment-jsdom: - specifier: ^30.1.2 - version: 30.1.2 + specifier: ^30.2.0 + version: 30.2.0 jest-fixed-jsdom: - specifier: ^0.0.10 - version: 0.0.10(jest-environment-jsdom@30.1.2) + specifier: ^0.0.11 + version: 0.0.11(jest-environment-jsdom@30.2.0) jiti: specifier: 1.21.7 version: 1.21.7 next-router-mock: - specifier: ^1.0.2 - version: 1.0.2(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + specifier: ^1.0.5 + version: 1.0.5(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) openapi-typescript: - specifier: ^7.9.1 - version: 7.9.1(typescript@5.8.3) + specifier: ^7.10.1 + version: 7.10.1(typescript@5.9.3) postcss: specifier: ^8.5.6 version: 8.5.6 prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 tailwindcss: - specifier: ^3.4.17 - version: 3.4.17(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + specifier: ^3.4.19 + version: 3.4.19(yaml@2.8.2) ts-toolbelt: specifier: ^9.6.0 version: 9.6.0 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 packages/ufb-react: dependencies: '@radix-ui/react-accordion': specifier: ^1.2.12 - version: 1.2.12(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-checkbox': specifier: ^1.3.3 - version: 1.3.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dialog': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dropdown-menu': specifier: ^2.1.16 - version: 2.1.16(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-label': - specifier: ^2.1.7 - version: 2.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^2.1.8 + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-menubar': specifier: ^1.1.16 - version: 1.1.16(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-popover': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-radio-group': specifier: ^1.3.8 - version: 1.3.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-scroll-area': specifier: ^1.2.10 - version: 1.2.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-select': specifier: ^2.2.6 - version: 2.2.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-separator': - specifier: ^1.1.7 - version: 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-slot': - specifier: ^1.2.3 - version: 1.2.3(@types/react@19.1.12)(react@19.1.1) + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.8)(react@19.2.3) '@radix-ui/react-switch': specifier: ^1.2.6 - version: 1.2.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-tabs': specifier: ^1.1.13 - version: 1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-toast': specifier: ^1.2.15 - version: 1.2.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-toggle': specifier: ^1.1.10 - version: 1.1.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-toggle-group': specifier: ^1.1.11 - version: 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-tooltip': specifier: ^1.2.8 - version: 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@remixicon/react': - specifier: ^4.6.0 - version: 4.6.0(react@19.1.1) + specifier: ^4.8.0 + version: 4.8.0(react@19.2.3) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -631,32 +707,32 @@ importers: version: 2.1.1 cmdk: specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) date-fns: specifier: ^4.1.0 version: 4.1.0 lucide-react: - specifier: ^0.543.0 - version: 0.543.0(react@19.1.1) + specifier: ^0.562.0 + version: 0.562.0(react@19.2.3) react-day-picker: specifier: 8.10.1 - version: 8.10.1(date-fns@4.1.0)(react@19.1.1) + version: 8.10.1(date-fns@4.1.0)(react@19.2.3) react-hook-form: - specifier: ^7.62.0 - version: 7.62.0(react@19.1.1) + specifier: ^7.71.1 + version: 7.71.1(react@19.2.3) sonner: specifier: ^2.0.7 - version: 2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tailwind-merge: - specifier: ^3.3.1 - version: 3.3.1 + specifier: ^3.4.0 + version: 3.4.0 devDependencies: '@types/react': - specifier: ^19.1.12 - version: 19.1.12 + specifier: ^19.2.8 + version: 19.2.8 '@types/react-dom': - specifier: ^19.1.9 - version: 19.1.9(@types/react@19.1.12) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.8) '@ufb/eslint-config': specifier: workspace:^ version: link:../../tooling/eslint @@ -671,19 +747,19 @@ importers: version: link:../../tooling/typescript eslint: specifier: 'catalog:' - version: 9.35.0(jiti@2.4.2) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 react: - specifier: ^19.1.1 - version: 19.1.1 + specifier: ^19.2.3 + version: 19.2.3 tailwindcss: specifier: 'catalog:' - version: 3.4.17(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + version: 3.4.19(yaml@2.8.2) typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 packages/ufb-shared: devDependencies: @@ -698,19 +774,19 @@ importers: version: link:../../tooling/typescript eslint: specifier: 'catalog:' - version: 9.35.0(jiti@2.4.2) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 react: - specifier: ^19.1.1 - version: 19.1.1 + specifier: ^19.2.3 + version: 19.2.3 tsup: - specifier: ^8.5.0 - version: 8.5.0(@swc/core@1.13.5(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.7.0) + specifier: ^8.5.1 + version: 8.5.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 packages/ufb-tailwindcss: devDependencies: @@ -724,14 +800,14 @@ importers: specifier: workspace:^ version: link:../../tooling/typescript autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.6) eslint: specifier: 'catalog:' - version: 9.35.0(jiti@2.4.2) + version: 9.39.2(jiti@1.21.7) glob: - specifier: ^11.0.3 - version: 11.0.3 + specifier: ^13.0.0 + version: 13.0.0 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -739,50 +815,50 @@ importers: specifier: ^16.1.1 version: 16.1.1(postcss@8.5.6) postcss-js: - specifier: ^4.0.1 - version: 4.0.1(postcss@8.5.6) + specifier: ^5.0.3 + version: 5.0.3(postcss@8.5.6) postcss-nesting: - specifier: ^13.0.2 - version: 13.0.2(postcss@8.5.6) + specifier: ^14.0.0 + version: 14.0.0(postcss@8.5.6) prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 tailwindcss: specifier: 'catalog:' - version: 3.4.17(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + version: 3.4.19(yaml@2.8.2) tooling/eslint: dependencies: '@eslint/compat': - specifier: ^1.3.2 - version: 1.3.2(eslint@9.35.0(jiti@2.4.2)) + specifier: ^2.0.1 + version: 2.0.1(eslint@9.39.2(jiti@1.21.7)) '@next/eslint-plugin-next': - specifier: ^15.4.7 - version: 15.4.7 + specifier: ^16.1.3 + version: 16.1.3 '@ufb/eslint-plugin-header': specifier: workspace:* version: link:../eslint-plugin-header eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.35.0(jiti@2.4.2)) + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.35.0(jiti@2.4.2)) + version: 6.10.2(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.35.0(jiti@2.4.2)) + version: 7.37.5(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-react-compiler: specifier: beta - version: 19.0.0-beta-af1b7da-20250417(eslint@9.35.0(jiti@2.4.2)) + version: 19.0.0-beta-af1b7da-20250417(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-react-hooks: - specifier: ^5.2.0 - version: 5.2.0(eslint@9.35.0(jiti@2.4.2)) + specifier: ^6.1.1 + version: 6.1.1(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-turbo: - specifier: ^2.5.6 - version: 2.5.6(eslint@9.35.0(jiti@2.4.2))(turbo@2.5.6) + specifier: ^2.7.5 + version: 2.7.5(eslint@9.39.2(jiti@1.21.7))(turbo@2.7.5) typescript-eslint: - specifier: ^8.43.0 - version: 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + specifier: ^8.46.0 + version: 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) devDependencies: '@ufb/prettier-config': specifier: workspace:* @@ -792,13 +868,13 @@ importers: version: link:../typescript eslint: specifier: 'catalog:' - version: 9.35.0(jiti@2.4.2) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 tooling/eslint-plugin-header: {} @@ -808,37 +884,116 @@ importers: dependencies: '@ianvs/prettier-plugin-sort-imports': specifier: ^4.7.0 - version: 4.7.0(prettier@3.5.3) + version: 4.7.0(prettier@3.6.2) prettier: specifier: 'catalog:' - version: 3.5.3 + version: 3.6.2 prettier-plugin-tailwindcss: - specifier: ^0.6.14 - version: 0.6.14(@ianvs/prettier-plugin-sort-imports@4.7.0(prettier@3.5.3))(prettier@3.5.3) + specifier: ^0.7.2 + version: 0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.0(prettier@3.6.2))(prettier@3.6.2) devDependencies: '@types/node': - specifier: 22.18.1 - version: 22.18.1 + specifier: 24.10.8 + version: 24.10.8 '@ufb/tsconfig': specifier: workspace:* version: link:../typescript typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 tooling/typescript: {} packages: - '@adobe/css-tools@4.4.0': - resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@algolia/abtesting@1.9.0': + resolution: {integrity: sha512-4q9QCxFPiDIx1n5w41A1JMkrXI8p0ugCQnCGFtCKZPmWtwgWCqwVRncIbp++81xSELFZVQUfiB7Kbsla1tIBSw==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.17.9': + resolution: {integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.9': + resolution: {integrity: sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.9': + resolution: {integrity: sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.9': + resolution: {integrity: sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.43.0': + resolution: {integrity: sha512-YsKYkohIMxiYEAu8nppZi5EioYDUIo9Heoor8K8vMUnkUtGCOEU/Q4p5OWaYSSBx3evo09Ga9rG4jsKViIcDzQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.43.0': + resolution: {integrity: sha512-kDGJWt3nzf0nu5RPFXQhNGl6Q0cn35fazxVWXhd0Fw3Vo6gcVfrcezcBenHb66laxnVJ7uwr1uKhmsu3Wy25sQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.43.0': + resolution: {integrity: sha512-RAFipkAnI8xhL/Sgi/gpXgNWN5HDM6F7z4NNNOcI8ZMYysZEBsqVXojg/WdKEKkQCOHVTZ3mooIjc5BaQdyVtA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.43.0': + resolution: {integrity: sha512-PmVs83THco8Qig3cAjU9a5eAGaSxsfgh7PdmWMQFE/MCmIcLPv0MVpgfcGGyPjZGYvPC4cg+3q7JJxcNSsEaTg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.43.0': + resolution: {integrity: sha512-Bs4zMLXvkAr19FSOZWNizlNUpRFxZVxtvyEJ+q3n3+hPZUcKjo0LIh15qghhRcQPEihjBN6Gr/U+AqRfOCsvnA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.43.0': + resolution: {integrity: sha512-pwHv+z8TZAKbwAWt9+v2gIqlqcCFiMdteTdgdPn2yOBRx4WUQdsIWAaG9GiV3by8jO51FuFQnTohhauuI63y3A==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.43.0': + resolution: {integrity: sha512-wKy6x6fKcnB1CsfeNNdGp4dzLzz04k8II3JLt6Sp81F8s57Ks3/K9qsysmL9SJa8P486s719bBttVLE8JJYurQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/events@4.0.1': + resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} + + '@algolia/ingestion@1.43.0': + resolution: {integrity: sha512-TA21h2KwqCUyPXhSAWF3R2UES/FAnzjaVPDI6cRPXeadX+pdrGN0GWat5gSUATJVcMHECn+lGvuMMRxO86o2Pg==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.43.0': + resolution: {integrity: sha512-rvWVEiA1iLcFmHS3oIXGIBreHIxNZqEFDjiNyRtLEffgd62kul2DjXM7H5bOouDMTo1ywMWT9OeQnzrhlTGAwA==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.43.0': + resolution: {integrity: sha512-scCijGd38npvH2uHbYhO4f1SR8It5R2FZqOjNcMfw/7Ph7Hxvl+cd7Mo6RzIxsNRcLW5RrwjtpTK3gpDe8r/WQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.43.0': + resolution: {integrity: sha512-jMkRLWJYr4Hcmpl89e4vIWs69Mkf8Uwx7MG5ZKk2UxW3G3TmouGjI0Ph5mVPmg3Jf1UG3AdmVDc4XupzycT1Jw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.43.0': + resolution: {integrity: sha512-KyQiVz+HdYtissC0J9KIGhHhKytQyJX+82GVsbv5rSCXbETnAoojvUyCn+3KRtWUvMDYCsZ+Y7hM71STTUJUJg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.43.0': + resolution: {integrity: sha512-UnUBNY0U+oT0bkYDsEqVsCkErC2w7idk4CRiLSzicqY8tGylD9oP0j13X/fse1CuiAFCCr3jfl+cBlN6dC0OFw==} + engines: {node: '>= 14.0.0'} '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@angular-devkit/core@19.2.15': - resolution: {integrity: sha512-pU2RZYX6vhd7uLSdLwPnuBcr0mXJSjp3EgOXKsrlQFQZevc+Qs+2JdXgIElnOT/aDqtRtriDmLlSbtdE8n3ZbA==} + '@angular-devkit/core@19.2.17': + resolution: {integrity: sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^4.0.0 @@ -846,20 +1001,27 @@ packages: chokidar: optional: true - '@angular-devkit/schematics-cli@19.2.15': - resolution: {integrity: sha512-1ESFmFGMpGQmalDB3t2EtmWDGv6gOFYBMxmHO2f1KI/UDl8UmZnCGL4mD3EWo8Hv0YIsZ9wOH9Q7ZHNYjeSpzg==} + '@angular-devkit/core@19.2.19': + resolution: {integrity: sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - hasBin: true + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true - '@angular-devkit/schematics@19.2.15': - resolution: {integrity: sha512-kNOJ+3vekJJCQKWihNmxBkarJzNW09kP5a9E1SRNiQVNOUEeSwcRR0qYotM65nx821gNzjjhJXnAZ8OazWldrg==} + '@angular-devkit/schematics-cli@19.2.19': + resolution: {integrity: sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true - '@ark/schema@0.46.0': - resolution: {integrity: sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ==} + '@angular-devkit/schematics@19.2.17': + resolution: {integrity: sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@ark/util@0.46.0': - resolution: {integrity: sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg==} + '@angular-devkit/schematics@19.2.19': + resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -887,218 +1049,230 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-s3@3.884.0': - resolution: {integrity: sha512-Okwg52/iho5wMCzd7A70g50k+bEYS+VUbSjD3NOzwrvZ3c7DwYjDUt13hVnUwMGygTVL+NGSpXSziTZe9P3/Cg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-s3@3.971.0': + resolution: {integrity: sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/client-sesv2@3.864.0': - resolution: {integrity: sha512-pwn4/3bs7ccucS9sYpMbzptEhEFQQy8TXtmKNzmyY7OIDBGTiJrxsWYDTULO4nxsMmGXi39mSEowlK4QUCyC+w==} + '@aws-sdk/client-sesv2@3.925.0': + resolution: {integrity: sha512-DtW+6xU/DTWFi3sOSNmi4NAQC18wPSURyYlNVwdUrr7w2ejSXjY2uvdGNlBJInKYA59FoZcmJuRM1RlR96LWfg==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.864.0': - resolution: {integrity: sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==} + '@aws-sdk/client-sso@3.925.0': + resolution: {integrity: sha512-ixC9CyXe/mBo1X+bzOxIIzsdBYzM+klWoHUYzwnPMrXhpDrMjj8D24R/FPqrDnhoYYXiyS4BApRLpeymsFJq2Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.883.0': - resolution: {integrity: sha512-Ybjw76yPceEBO7+VLjy5+/Gr0A1UNymSDHda5w8tfsS2iHZt/vuD6wrYpHdLoUx4H5la8ZhwcSfK/+kmE+QLPw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-sso@3.971.0': + resolution: {integrity: sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.864.0': - resolution: {integrity: sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==} + '@aws-sdk/core@3.922.0': + resolution: {integrity: sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/core@3.883.0': - resolution: {integrity: sha512-FmkqnqBLkXi4YsBPbF6vzPa0m4XKUuvgKDbamfw4DZX2CzfBZH6UU4IwmjNV3ZM38m0xraHarK8KIbGSadN3wg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.970.0': + resolution: {integrity: sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.864.0': - resolution: {integrity: sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/crc64-nvme@3.969.0': + resolution: {integrity: sha512-IGNkP54HD3uuLnrPCYsv3ZD478UYq+9WwKrIVJ9Pdi3hxPg8562CH3ZHf8hEgfePN31P9Kj+Zu9kq2Qcjjt61A==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.883.0': - resolution: {integrity: sha512-Z6tPBXPCodfhIF1rvQKoeRGMkwL6TK0xdl1UoMIA1x4AfBpPICAF77JkFBExk/pdiFYq1d04Qzddd/IiujSlLg==} + '@aws-sdk/credential-provider-env@3.922.0': + resolution: {integrity: sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-http@3.864.0': - resolution: {integrity: sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.970.0': + resolution: {integrity: sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.883.0': - resolution: {integrity: sha512-P589ug1lMOOEYLTaQJjSP+Gee34za8Kk2LfteNQfO9SpByHFgGj++Sg8VyIe30eZL8Q+i4qTt24WDCz1c+dgYg==} + '@aws-sdk/credential-provider-http@3.922.0': + resolution: {integrity: sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-ini@3.864.0': - resolution: {integrity: sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.970.0': + resolution: {integrity: sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.883.0': - resolution: {integrity: sha512-n6z9HTzuDEdugXvPiE/95VJXbF4/gBffdV/SRHDJKtDHaRuvp/gggbfmfVSTFouGVnlKPb2pQWQsW3Nr/Y3Lrw==} + '@aws-sdk/credential-provider-ini@3.925.0': + resolution: {integrity: sha512-TOs/UkKWwXrSPolRTChpDUQjczw6KqbbanF0EzjUm3sp/AS1ThOQCKuTTdaOBZXkCIJdvRmZjF3adccE3rAoXg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-node@3.864.0': - resolution: {integrity: sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.971.0': + resolution: {integrity: sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.883.0': - resolution: {integrity: sha512-QIUhsatsrwfB9ZsKpmi0EySSfexVP61wgN7hr493DOileh2QsKW4XATEfsWNmx0dj9323Vg1Mix7bXtRfl9cGg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-login@3.971.0': + resolution: {integrity: sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.864.0': - resolution: {integrity: sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==} + '@aws-sdk/credential-provider-node@3.925.0': + resolution: {integrity: sha512-+T9mnnTY73MLkVxsk5RtzE4fv7GnMhR7iXhL/yTusf1zLfA09uxlA9VCz6tWxm5rHcO4ZN0x4hnqqDhM+DB5KQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-process@3.883.0': - resolution: {integrity: sha512-m1shbHY/Vppy4EdddG9r8x64TO/9FsCjokp5HbKcZvVoTOTgUJrdT8q2TAQJ89+zYIJDqsKbqfrmfwJ1zOdnGQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.971.0': + resolution: {integrity: sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.864.0': - resolution: {integrity: sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==} + '@aws-sdk/credential-provider-process@3.922.0': + resolution: {integrity: sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-sso@3.883.0': - resolution: {integrity: sha512-37ve9Tult08HLXrJFHJM/sGB/vO7wzI6v1RUUfeTiShqx8ZQ5fTzCTNY/duO96jCtCexmFNSycpQzh7lDIf0aA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.970.0': + resolution: {integrity: sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.864.0': - resolution: {integrity: sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==} + '@aws-sdk/credential-provider-sso@3.925.0': + resolution: {integrity: sha512-aZlUC6LRsOMDvIu0ifF62mTjL3KGzclWu5XBBN8eLDAYTdhqMxv3HyrqWoiHnGZnZGaVU+II+qsVoeBnGOwHow==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-web-identity@3.883.0': - resolution: {integrity: sha512-SL82K9Jb0vpuTadqTO4Fpdu7SKtebZ3Yo4LZvk/U0UauVMlJj5ZTos0mFx1QSMB9/4TpqifYrSZcdnxgYg8Eqw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.971.0': + resolution: {integrity: sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-bucket-endpoint@3.873.0': - resolution: {integrity: sha512-b4bvr0QdADeTUs+lPc9Z48kXzbKHXQKgTvxx/jXDgSW9tv4KmYPO1gIj6Z9dcrBkRWQuUtSW3Tu2S5n6pe+zeg==} + '@aws-sdk/credential-provider-web-identity@3.925.0': + resolution: {integrity: sha512-dR34s8Sfd1wJBzIuvRFO2FCnLmYD8iwPWrdXWI2ZypFt1EQR8jeQ20mnS+UOCoR5Z0tY6wJqEgTXKl4KuZ+DUg==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-expect-continue@3.873.0': - resolution: {integrity: sha512-GIqoc8WgRcf/opBOZXFLmplJQKwOMjiOMmDz9gQkaJ8FiVJoAp8EGVmK2TOWZMQUYsavvHYsHaor5R2xwPoGVg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.971.0': + resolution: {integrity: sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.883.0': - resolution: {integrity: sha512-EloU4ZjkH+CXCHJcYElXo5nZ1vK6Miam/S02YSHk5JTrJkm4RV478KXXO29TIIAwZXcLT/FEQOZ9ZH/JHFFCFQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-bucket-endpoint@3.969.0': + resolution: {integrity: sha512-MlbrlixtkTVhYhoasblKOkr7n2yydvUZjjxTnBhIuHmkyBS1619oGnTfq/uLeGYb4NYXdeQ5OYcqsRGvmWSuTw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.862.0': - resolution: {integrity: sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-expect-continue@3.969.0': + resolution: {integrity: sha512-qXygzSi8osok7tH9oeuS3HoKw6jRfbvg5Me/X5RlHOvSSqQz8c5O9f3MjUApaCUSwbAU92KrbZWasw2PKiaVHg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.873.0': - resolution: {integrity: sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-flexible-checksums@3.971.0': + resolution: {integrity: sha512-+hGUDUxeIw8s2kkjfeXym0XZxdh0cqkHkDpEanWYdS1gnWkIR+gf9u/DKbKqGHXILPaqHXhWpLTQTVlaB4sI7Q==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-location-constraint@3.873.0': - resolution: {integrity: sha512-r+hIaORsW/8rq6wieDordXnA/eAu7xAPLue2InhoEX6ML7irP52BgiibHLpt9R0psiCzIHhju8qqKa4pJOrmiw==} + '@aws-sdk/middleware-host-header@3.922.0': + resolution: {integrity: sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-logger@3.862.0': - resolution: {integrity: sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-host-header@3.969.0': + resolution: {integrity: sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.876.0': - resolution: {integrity: sha512-cpWJhOuMSyz9oV25Z/CMHCBTgafDCbv7fHR80nlRrPdPZ8ETNsahwRgltXP1QJJ8r3X/c1kwpOR7tc+RabVzNA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-location-constraint@3.969.0': + resolution: {integrity: sha512-zH7pDfMLG/C4GWMOpvJEoYcSpj7XsNP9+irlgqwi667sUQ6doHQJ3yyDut3yiTk0maq1VgmriPFELyI9lrvH/g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.862.0': - resolution: {integrity: sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==} + '@aws-sdk/middleware-logger@3.922.0': + resolution: {integrity: sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-recursion-detection@3.873.0': - resolution: {integrity: sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.969.0': + resolution: {integrity: sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.864.0': - resolution: {integrity: sha512-GjYPZ6Xnqo17NnC8NIQyvvdzzO7dm+Ks7gpxD/HsbXPmV2aEfuFveJXneGW9e1BheSKFff6FPDWu8Gaj2Iu1yg==} + '@aws-sdk/middleware-recursion-detection@3.922.0': + resolution: {integrity: sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-sdk-s3@3.883.0': - resolution: {integrity: sha512-i4sGOj9xhSN6/LkYj3AJ2SRWENnpN9JySwNqIoRqO1Uon8gfyNLJd1yV+s43vXQsU5wbKWVXK8l9SRo+vNTQwg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.969.0': + resolution: {integrity: sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-ssec@3.873.0': - resolution: {integrity: sha512-AF55J94BoiuzN7g3hahy0dXTVZahVi8XxRBLgzNp6yQf0KTng+hb/V9UQZVYY1GZaDczvvvnqC54RGe9OZZ9zQ==} + '@aws-sdk/middleware-sdk-s3@3.922.0': + resolution: {integrity: sha512-ygg8lME1oFAbsH42ed2wtGqfHLoT5irgx6VC4X98j79fV1qXEwwwbqMsAiMQ/HJehpjqAFRVsHox3MHLN48Z5A==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-user-agent@3.864.0': - resolution: {integrity: sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-sdk-s3@3.970.0': + resolution: {integrity: sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.883.0': - resolution: {integrity: sha512-q58uLYnGLg7hsnWpdj7Cd1Ulsq1/PUJOHvAfgcBuiDE/+Fwh0DZxZZyjrU+Cr+dbeowIdUaOO8BEDDJ0CUenJw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-ssec@3.971.0': + resolution: {integrity: sha512-QGVhvRveYG64ZhnS/b971PxXM6N2NU79Fxck4EfQ7am8v1Br0ctoeDDAn9nXNblLGw87we9Z65F7hMxxiFHd3w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.864.0': - resolution: {integrity: sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==} + '@aws-sdk/middleware-user-agent@3.922.0': + resolution: {integrity: sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/nested-clients@3.883.0': - resolution: {integrity: sha512-IhzDM+v0ga53GOOrZ9jmGNr7JU5OR6h6ZK9NgB7GXaa+gsDbqfUuXRwyKDYXldrTXf1sUR3vy1okWDXA7S2ejQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-user-agent@3.970.0': + resolution: {integrity: sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.862.0': - resolution: {integrity: sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==} + '@aws-sdk/nested-clients@3.925.0': + resolution: {integrity: sha512-Fc8QhH+1YzGQb5aWQUX6gRnKSzUZ9p3p/muqXIgYBL8RSd5O6hSPhDTyrOWE247zFlOjVlAlEnoTMJKarH0cIA==} engines: {node: '>=18.0.0'} - '@aws-sdk/region-config-resolver@3.873.0': - resolution: {integrity: sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.971.0': + resolution: {integrity: sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/s3-request-presigner@3.884.0': - resolution: {integrity: sha512-dYc4p07S9u3iE8wNNqsfQcKzRii87q6o8/IZ1loaLIZHTOgZ34JGWbLq6fGgLpxx+kpIKaRNTqEgNgN6q5PaXg==} + '@aws-sdk/region-config-resolver@3.925.0': + resolution: {integrity: sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/signature-v4-multi-region@3.864.0': - resolution: {integrity: sha512-w2HIn/WIcUyv1bmyCpRUKHXB5KdFGzyxPkp/YK5g+/FuGdnFFYWGfcO8O+How4jwrZTarBYsAHW9ggoKvwr37w==} - engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.969.0': + resolution: {integrity: sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.883.0': - resolution: {integrity: sha512-86PO7+xhuQ48cD3xlZgEpRxVP1lBarWAJy23sB6zZLHgZSbnYXYjRFuyxX4PlFzqllM3PDKJvq3WnXeqSXeNsg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/s3-request-presigner@3.971.0': + resolution: {integrity: sha512-j4wCCoQ//xm03JQn7/Jq6BJ0HV3VzlI/HrIQSQupWWjZTrdxyqa9PXBhcYNNtvZtF1adA/cRpYTMS+2SUsZGRg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.864.0': - resolution: {integrity: sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==} + '@aws-sdk/signature-v4-multi-region@3.922.0': + resolution: {integrity: sha512-mmsgEEL5pE+A7gFYiJMDBCLVciaXq4EFI5iAP7bPpnHvOplnNOYxVy2IreKMllGvrfjVyLnwxzZYlo5zZ65FWg==} engines: {node: '>=18.0.0'} - '@aws-sdk/token-providers@3.883.0': - resolution: {integrity: sha512-tcj/Z5paGn9esxhmmkEW7gt39uNoIRbXG1UwJrfKu4zcTr89h86PDiIE2nxUO3CMQf1KgncPpr5WouPGzkh/QQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/signature-v4-multi-region@3.970.0': + resolution: {integrity: sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.862.0': - resolution: {integrity: sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==} + '@aws-sdk/token-providers@3.925.0': + resolution: {integrity: sha512-F4Oibka1W5YYDeL+rGt/Hg3NLjOzrJdmuZOE0OFQt/U6dnJwYmYi2gFqduvZnZcD1agNm37mh7/GUq1zvKS6ig==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-arn-parser@3.804.0': - resolution: {integrity: sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.971.0': + resolution: {integrity: sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-arn-parser@3.873.0': - resolution: {integrity: sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==} + '@aws-sdk/types@3.922.0': + resolution: {integrity: sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-endpoints@3.862.0': - resolution: {integrity: sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.969.0': + resolution: {integrity: sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.879.0': - resolution: {integrity: sha512-aVAJwGecYoEmbEFju3127TyJDF9qJsKDUUTRMDuS8tGn+QiWQFnfInmbt+el9GU1gEJupNTXV+E3e74y51fb7A==} + '@aws-sdk/util-arn-parser@3.893.0': + resolution: {integrity: sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-format-url@3.873.0': - resolution: {integrity: sha512-v//b9jFnhzTKKV3HFTw2MakdM22uBAs2lBov51BWmFXuFtSTdBLrR7zgfetQPE3PVkFai0cmtJQPdc3MX+T/cQ==} + '@aws-sdk/util-arn-parser@3.968.0': + resolution: {integrity: sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.922.0': + resolution: {integrity: sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-locate-window@3.723.0': - resolution: {integrity: sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==} + '@aws-sdk/util-endpoints@3.970.0': + resolution: {integrity: sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.969.0': + resolution: {integrity: sha512-C7ZiE8orcrEF9In+XDlIKrZhMjp0HCPUH6u74pgadE3T2LRre5TmOQcTt785/wVS2G0we9cxkjlzMrfDsfPvFw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.893.0': + resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-user-agent-browser@3.862.0': - resolution: {integrity: sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==} + '@aws-sdk/util-user-agent-browser@3.922.0': + resolution: {integrity: sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==} - '@aws-sdk/util-user-agent-browser@3.873.0': - resolution: {integrity: sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==} + '@aws-sdk/util-user-agent-browser@3.969.0': + resolution: {integrity: sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==} - '@aws-sdk/util-user-agent-node@3.864.0': - resolution: {integrity: sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==} + '@aws-sdk/util-user-agent-node@3.922.0': + resolution: {integrity: sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==} engines: {node: '>=18.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -1106,41 +1280,57 @@ packages: aws-crt: optional: true - '@aws-sdk/util-user-agent-node@3.883.0': - resolution: {integrity: sha512-28cQZqC+wsKUHGpTBr+afoIdjS6IoEJkMqcZsmo2Ag8LzmTa6BUWQenFYB0/9BmDy4PZFPUn+uX+rJgWKB+jzA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-user-agent-node@3.971.0': + resolution: {integrity: sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==} + engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' peerDependenciesMeta: aws-crt: optional: true - '@aws-sdk/xml-builder@3.862.0': - resolution: {integrity: sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==} + '@aws-sdk/xml-builder@3.921.0': + resolution: {integrity: sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/xml-builder@3.969.0': + resolution: {integrity: sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.1.1': + resolution: {integrity: sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==} engines: {node: '>=18.0.0'} - '@aws-sdk/xml-builder@3.873.0': - resolution: {integrity: sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==} + '@aws/lambda-invoke-store@0.2.2': + resolution: {integrity: sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==} engines: {node: '>=18.0.0'} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.0': - resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + '@babel/core@7.28.6': + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.24.7': - resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': @@ -1151,20 +1341,18 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.24.8': - resolution: {integrity: sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA==} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-create-class-features-plugin@7.27.1': - resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + '@babel/helper-create-class-features-plugin@7.28.5': + resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.27.1': - resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1174,48 +1362,38 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - '@babel/helper-environment-visitor@7.24.7': - resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.24.7': - resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} - engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.24.8': - resolution: {integrity: sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.27.1': - resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.3': resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.24.7': - resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 '@babel/helper-optimise-call-expression@7.27.1': resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.24.8': - resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} - engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.27.1': resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} @@ -1226,67 +1404,48 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-replace-supers@7.24.7': - resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-replace-supers@7.27.1': resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-skip-transparent-expression-wrappers@7.24.7': - resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} engines: {node: '>=6.9.0'} - '@babel/helper-split-export-declaration@7.24.7': - resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.27.1': - resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} + '@babel/helper-wrap-function@7.28.3': + resolution: {integrity: sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.4': - resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.10': - resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.28.3': - resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': - resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1309,8 +1468,8 @@ packages: peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1': - resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': + resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1349,6 +1508,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.27.1': resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} @@ -1455,8 +1619,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.28.0': - resolution: {integrity: sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==} + '@babel/plugin-transform-block-scoping@7.28.5': + resolution: {integrity: sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1467,14 +1631,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.27.1': - resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==} + '@babel/plugin-transform-class-static-block@7.28.3': + resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.28.0': - resolution: {integrity: sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==} + '@babel/plugin-transform-classes@7.28.4': + resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1485,8 +1649,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.28.0': - resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1521,8 +1685,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.27.1': - resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} + '@babel/plugin-transform-exponentiation-operator@7.28.5': + resolution: {integrity: sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1557,8 +1721,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.27.1': - resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} + '@babel/plugin-transform-logical-assignment-operators@7.28.5': + resolution: {integrity: sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1581,8 +1745,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.27.1': - resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} + '@babel/plugin-transform-modules-systemjs@7.28.5': + resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1617,8 +1781,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.28.0': - resolution: {integrity: sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==} + '@babel/plugin-transform-object-rest-spread@7.28.4': + resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1635,8 +1799,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.27.1': - resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} + '@babel/plugin-transform-optional-chaining@7.28.5': + resolution: {integrity: sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1695,8 +1859,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.28.1': - resolution: {integrity: sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==} + '@babel/plugin-transform-regenerator@7.28.4': + resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1713,6 +1877,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-runtime@7.28.5': + resolution: {integrity: sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-shorthand-properties@7.27.1': resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} engines: {node: '>=6.9.0'} @@ -1743,8 +1913,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.28.0': - resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + '@babel/plugin-transform-typescript@7.28.5': + resolution: {integrity: sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1773,8 +1943,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.28.0': - resolution: {integrity: sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==} + '@babel/preset-env@7.28.5': + resolution: {integrity: sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1784,53 +1954,56 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - '@babel/preset-react@7.27.1': - resolution: {integrity: sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==} + '@babel/preset-react@7.28.5': + resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.27.1': - resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime-corejs3@7.26.0': - resolution: {integrity: sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.27.1': - resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} + '@babel/runtime-corejs3@7.28.4': + resolution: {integrity: sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.2': - resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.3': - resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@borewit/text-codec@0.1.1': + resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -1918,8 +2091,15 @@ packages: resolution: {integrity: sha512-u4eku+hnPqqHIGq/ZUQcaP0TrCbYeLIYBaK7qClNRGZbnh8RC4gVxLEIo8Pceo1nOK9E5G4Lxzlw5KnXcvflfA==} engines: {node: '>= 10'} - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + '@csstools/cascade-layer-name-parser@2.0.5': + resolution: {integrity: sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} '@csstools/css-calc@2.1.4': @@ -1929,8 +2109,8 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.10': - resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.5 @@ -1946,735 +2126,1095 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@csstools/selector-resolve-nested@3.1.0': - resolution: {integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==} + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} engines: {node: '>=18'} peerDependencies: - postcss-selector-parser: ^7.0.0 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/selector-specificity@5.0.0': - resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + '@csstools/postcss-alpha-function@1.0.1': + resolution: {integrity: sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==} engines: {node: '>=18'} peerDependencies: - postcss-selector-parser: ^7.0.0 + postcss: ^8.4 - '@dnd-kit/accessibility@3.1.1': - resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + '@csstools/postcss-cascade-layers@5.0.2': + resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==} + engines: {node: '>=18'} peerDependencies: - react: '>=16.8.0' + postcss: ^8.4 - '@dnd-kit/core@6.3.1': - resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + '@csstools/postcss-color-function-display-p3-linear@1.0.1': + resolution: {integrity: sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==} + engines: {node: '>=18'} peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + postcss: ^8.4 - '@dnd-kit/modifiers@9.0.0': - resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + '@csstools/postcss-color-function@4.0.12': + resolution: {integrity: sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==} + engines: {node: '>=18'} peerDependencies: - '@dnd-kit/core': ^6.3.0 - react: '>=16.8.0' + postcss: ^8.4 - '@dnd-kit/sortable@10.0.0': - resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + '@csstools/postcss-color-mix-function@3.0.12': + resolution: {integrity: sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==} + engines: {node: '>=18'} peerDependencies: - '@dnd-kit/core': ^6.3.0 - react: '>=16.8.0' + postcss: ^8.4 - '@dnd-kit/utilities@3.2.2': - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2': + resolution: {integrity: sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==} + engines: {node: '>=18'} peerDependencies: - react: '>=16.8.0' - - '@emnapi/core@1.4.5': - resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} - - '@emnapi/runtime@1.4.5': - resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} - - '@emnapi/wasi-threads@1.0.4': - resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} - - '@emotion/babel-plugin@11.13.5': - resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} - - '@emotion/cache@11.14.0': - resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} - - '@emotion/hash@0.9.2': - resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - - '@emotion/memoize@0.9.0': - resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + postcss: ^8.4 - '@emotion/react@11.14.0': - resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + '@csstools/postcss-content-alt-text@2.0.8': + resolution: {integrity: sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==} + engines: {node: '>=18'} peerDependencies: - '@types/react': '*' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - - '@emotion/serialize@1.3.3': - resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} - - '@emotion/sheet@1.4.0': - resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} - - '@emotion/unitless@0.10.0': - resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + postcss: ^8.4 - '@emotion/use-insertion-effect-with-fallbacks@1.2.0': - resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + '@csstools/postcss-contrast-color-function@2.0.12': + resolution: {integrity: sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==} + engines: {node: '>=18'} peerDependencies: - react: '>=16.8.0' - - '@emotion/utils@1.4.2': - resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} - - '@emotion/weak-memoize@0.4.0': - resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + postcss: ^8.4 - '@esbuild/aix-ppc64@0.25.0': - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + '@csstools/postcss-exponential-functions@2.0.9': + resolution: {integrity: sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==} engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] + peerDependencies: + postcss: ^8.4 - '@esbuild/android-arm64@0.25.0': - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + '@csstools/postcss-font-format-keywords@4.0.0': + resolution: {integrity: sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==} engines: {node: '>=18'} - cpu: [arm64] - os: [android] + peerDependencies: + postcss: ^8.4 - '@esbuild/android-arm@0.25.0': - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + '@csstools/postcss-gamut-mapping@2.0.11': + resolution: {integrity: sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==} engines: {node: '>=18'} - cpu: [arm] - os: [android] + peerDependencies: + postcss: ^8.4 - '@esbuild/android-x64@0.25.0': - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + '@csstools/postcss-gradients-interpolation-method@5.0.12': + resolution: {integrity: sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==} engines: {node: '>=18'} - cpu: [x64] - os: [android] + peerDependencies: + postcss: ^8.4 - '@esbuild/darwin-arm64@0.25.0': - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + '@csstools/postcss-hwb-function@4.0.12': + resolution: {integrity: sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==} engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] + peerDependencies: + postcss: ^8.4 - '@esbuild/darwin-x64@0.25.0': - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + '@csstools/postcss-ic-unit@4.0.4': + resolution: {integrity: sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==} engines: {node: '>=18'} - cpu: [x64] - os: [darwin] + peerDependencies: + postcss: ^8.4 - '@esbuild/freebsd-arm64@0.25.0': - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + '@csstools/postcss-initial@2.0.1': + resolution: {integrity: sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==} engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] + peerDependencies: + postcss: ^8.4 - '@esbuild/freebsd-x64@0.25.0': - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + '@csstools/postcss-is-pseudo-class@5.0.3': + resolution: {integrity: sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==} engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-arm64@0.25.0': - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + '@csstools/postcss-light-dark-function@2.0.11': + resolution: {integrity: sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==} engines: {node: '>=18'} - cpu: [arm64] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-arm@0.25.0': - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + '@csstools/postcss-logical-float-and-clear@3.0.0': + resolution: {integrity: sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==} engines: {node: '>=18'} - cpu: [arm] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-ia32@0.25.0': - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + '@csstools/postcss-logical-overflow@2.0.0': + resolution: {integrity: sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==} engines: {node: '>=18'} - cpu: [ia32] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-loong64@0.25.0': - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + '@csstools/postcss-logical-overscroll-behavior@2.0.0': + resolution: {integrity: sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==} engines: {node: '>=18'} - cpu: [loong64] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-mips64el@0.25.0': - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + '@csstools/postcss-logical-resize@3.0.0': + resolution: {integrity: sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==} engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-ppc64@0.25.0': - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + '@csstools/postcss-logical-viewport-units@3.0.4': + resolution: {integrity: sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==} engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-riscv64@0.25.0': - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + '@csstools/postcss-media-minmax@2.0.9': + resolution: {integrity: sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==} engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-s390x@0.25.0': - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5': + resolution: {integrity: sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==} engines: {node: '>=18'} - cpu: [s390x] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/linux-x64@0.25.0': - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + '@csstools/postcss-nested-calc@4.0.0': + resolution: {integrity: sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==} engines: {node: '>=18'} - cpu: [x64] - os: [linux] + peerDependencies: + postcss: ^8.4 - '@esbuild/netbsd-arm64@0.25.0': - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + '@csstools/postcss-normalize-display-values@4.0.0': + resolution: {integrity: sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==} engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] + peerDependencies: + postcss: ^8.4 - '@esbuild/netbsd-x64@0.25.0': - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + '@csstools/postcss-oklab-function@4.0.12': + resolution: {integrity: sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==} engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] + peerDependencies: + postcss: ^8.4 - '@esbuild/openbsd-arm64@0.25.0': - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + '@csstools/postcss-progressive-custom-properties@4.2.1': + resolution: {integrity: sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==} engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] + peerDependencies: + postcss: ^8.4 - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + '@csstools/postcss-random-function@2.0.1': + resolution: {integrity: sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==} engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] + peerDependencies: + postcss: ^8.4 - '@esbuild/sunos-x64@0.25.0': - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + '@csstools/postcss-relative-color-syntax@3.0.12': + resolution: {integrity: sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==} engines: {node: '>=18'} - cpu: [x64] - os: [sunos] + peerDependencies: + postcss: ^8.4 - '@esbuild/win32-arm64@0.25.0': - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + '@csstools/postcss-scope-pseudo-class@4.0.1': + resolution: {integrity: sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==} engines: {node: '>=18'} - cpu: [arm64] - os: [win32] + peerDependencies: + postcss: ^8.4 - '@esbuild/win32-ia32@0.25.0': - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + '@csstools/postcss-sign-functions@1.1.4': + resolution: {integrity: sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==} engines: {node: '>=18'} - cpu: [ia32] - os: [win32] + peerDependencies: + postcss: ^8.4 - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + '@csstools/postcss-stepped-value-functions@4.0.9': + resolution: {integrity: sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==} engines: {node: '>=18'} - cpu: [x64] - os: [win32] + peerDependencies: + postcss: ^8.4 - '@eslint-community/eslint-utils@4.8.0': - resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@csstools/postcss-text-decoration-shorthand@4.0.3': + resolution: {integrity: sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==} + engines: {node: '>=18'} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + postcss: ^8.4 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@csstools/postcss-trigonometric-functions@4.0.9': + resolution: {integrity: sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 - '@eslint/compat@1.3.2': - resolution: {integrity: sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@csstools/postcss-unset-value@4.0.0': + resolution: {integrity: sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==} + engines: {node: '>=18'} peerDependencies: - eslint: ^8.40 || 9 - peerDependenciesMeta: - eslint: - optional: true + postcss: ^8.4 - '@eslint/config-array@0.21.0': - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@csstools/selector-resolve-nested@3.1.0': + resolution: {integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 - '@eslint/config-helpers@0.3.1': - resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@csstools/selector-resolve-nested@4.0.0': + resolution: {integrity: sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 - '@eslint/core@0.15.2': - resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@csstools/selector-specificity@6.0.0': + resolution: {integrity: sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 - '@eslint/js@9.35.0': - resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@csstools/utilities@2.0.0': + resolution: {integrity: sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} - '@eslint/plugin-kit@0.3.5': - resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' - '@faker-js/faker@9.9.0': - resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} - engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' - '@fast-csv/format@4.3.5': - resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' - '@fast-csv/format@5.0.5': - resolution: {integrity: sha512-0P9SJXXnqKdmuWlLaTelqbrfdgN37Mvrb369J6eNmqL41IEIZQmV4sNM4GgAK2Dz3aH04J0HKGDMJFkYObThTw==} + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' - '@fast-csv/parse@4.3.6': - resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' - '@fast-csv/parse@5.0.5': - resolution: {integrity: sha512-M0IbaXZDbxfOnpVE5Kps/a6FGlILLhtLsvWd9qNH3d2TxNnpbNkFf3KD26OmJX6MHq7PdQAl5htStDwnuwHx6w==} + '@docsearch/css@3.9.0': + resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} - '@fastify/accept-negotiator@2.0.1': - resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + '@docsearch/react@3.9.0': + resolution: {integrity: sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true - '@fastify/ajv-compiler@4.0.2': - resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + '@docusaurus/babel@3.9.2': + resolution: {integrity: sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==} + engines: {node: '>=20.0'} - '@fastify/busboy@3.1.1': - resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@docusaurus/bundler@3.9.2': + resolution: {integrity: sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==} + engines: {node: '>=20.0'} + peerDependencies: + '@docusaurus/faster': '*' + peerDependenciesMeta: + '@docusaurus/faster': + optional: true - '@fastify/cors@11.1.0': - resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==} + '@docusaurus/core@3.9.2': + resolution: {integrity: sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==} + engines: {node: '>=20.0'} + hasBin: true + peerDependencies: + '@mdx-js/react': ^3.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@fastify/deepmerge@3.1.0': - resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==} + '@docusaurus/cssnano-preset@3.9.2': + resolution: {integrity: sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==} + engines: {node: '>=20.0'} - '@fastify/error@4.1.0': - resolution: {integrity: sha512-KeFcciOr1eo/YvIXHP65S94jfEEqn1RxTRBT1aJaHxY5FK0/GDXYozsQMMWlZoHgi8i0s+YtrLsgj/JkUUjSkQ==} + '@docusaurus/logger@3.9.2': + resolution: {integrity: sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==} + engines: {node: '>=20.0'} - '@fastify/fast-json-stringify-compiler@5.0.2': - resolution: {integrity: sha512-YdR7gqlLg1xZAQa+SX4sMNzQHY5pC54fu9oC5aYSUqBhyn6fkLkrdtKlpVdCNPlwuUuXA1PjFTEmvMF6ZVXVGw==} + '@docusaurus/mdx-loader@3.9.2': + resolution: {integrity: sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@fastify/formbody@8.0.2': - resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==} + '@docusaurus/module-type-aliases@3.9.2': + resolution: {integrity: sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==} + peerDependencies: + react: '*' + react-dom: '*' - '@fastify/forwarded@3.0.0': - resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==} + '@docusaurus/plugin-content-blog@3.9.2': + resolution: {integrity: sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==} + engines: {node: '>=20.0'} + peerDependencies: + '@docusaurus/plugin-content-docs': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@fastify/merge-json-schemas@0.2.1': - resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + '@docusaurus/plugin-content-docs@3.9.2': + resolution: {integrity: sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@fastify/middie@9.0.3': - resolution: {integrity: sha512-7OYovKXp9UKYeVMcjcFLMcSpoMkmcZmfnG+eAvtdiatN35W7c+r9y1dRfpA+pfFVNuHGGqI3W+vDTmjvcfLcMA==} + '@docusaurus/plugin-content-pages@3.9.2': + resolution: {integrity: sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@fastify/multipart@9.2.1': - resolution: {integrity: sha512-U4221XDMfzCUtfzsyV1/PkR4MNgKI0158vUUyn/oF2Tl6RxMc+N7XYLr5fZXQiEC+Fmw5zFaTjxsTGTgtDtK+g==} + '@docusaurus/plugin-css-cascade-layers@3.9.2': + resolution: {integrity: sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==} + engines: {node: '>=20.0'} - '@fastify/proxy-addr@5.0.0': - resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} + '@docusaurus/plugin-debug@3.9.2': + resolution: {integrity: sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@fastify/send@4.1.0': - resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} + '@docusaurus/plugin-google-analytics@3.9.2': + resolution: {integrity: sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@fastify/static@8.2.0': - resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==} + '@docusaurus/plugin-google-gtag@3.9.2': + resolution: {integrity: sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@floating-ui/core@1.7.0': - resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} + '@docusaurus/plugin-google-tag-manager@3.9.2': + resolution: {integrity: sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@floating-ui/dom@1.7.0': - resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} + '@docusaurus/plugin-sitemap@3.9.2': + resolution: {integrity: sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@floating-ui/react-dom@2.1.2': - resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + '@docusaurus/plugin-svgr@3.9.2': + resolution: {integrity: sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==} + engines: {node: '>=20.0'} peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@floating-ui/utils@0.2.9': - resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@docusaurus/preset-classic@3.9.2': + resolution: {integrity: sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@hapi/address@5.1.1': - resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} - engines: {node: '>=14.0.0'} + '@docusaurus/react-loadable@6.0.0': + resolution: {integrity: sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==} + peerDependencies: + react: '*' - '@hapi/formula@3.0.2': - resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==} + '@docusaurus/theme-classic@3.9.2': + resolution: {integrity: sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@hapi/hoek@11.0.7': - resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + '@docusaurus/theme-common@3.9.2': + resolution: {integrity: sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==} + engines: {node: '>=20.0'} + peerDependencies: + '@docusaurus/plugin-content-docs': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@hapi/pinpoint@2.0.1': - resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} + '@docusaurus/theme-search-algolia@3.9.2': + resolution: {integrity: sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==} + engines: {node: '>=20.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@hapi/tlds@1.1.2': - resolution: {integrity: sha512-1jkwm1WY9VIb6WhdANRmWDkXQUcIRpxqZpSdS+HD9vhoVL3zwoFvP8doQNEgT6k3VST0Ewu50wZnXIceRYp5tw==} - engines: {node: '>=14.0.0'} + '@docusaurus/theme-translations@3.9.2': + resolution: {integrity: sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==} + engines: {node: '>=20.0'} - '@hapi/topo@6.0.2': - resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + '@docusaurus/tsconfig@3.9.2': + resolution: {integrity: sha512-j6/Fp4Rlpxsc632cnRnl5HpOWeb6ZKssDj6/XzzAzVGXXfm9Eptx3rxCC+fDzySn9fHTS+CWJjPineCR1bB5WQ==} - '@hookform/resolvers@5.2.1': - resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==} + '@docusaurus/types@3.9.2': + resolution: {integrity: sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==} peerDependencies: - react-hook-form: ^7.55.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} + '@docusaurus/utils-common@3.9.2': + resolution: {integrity: sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==} + engines: {node: '>=20.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} - engines: {node: '>=18.18.0'} + '@docusaurus/utils-validation@3.9.2': + resolution: {integrity: sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==} + engines: {node: '>=20.0'} - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + '@docusaurus/utils@3.9.2': + resolution: {integrity: sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==} + engines: {node: '>=20.0'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} + '@emnapi/core@1.7.0': + resolution: {integrity: sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==} - '@humanwhocodes/retry@0.4.2': - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} - engines: {node: '>=18.18'} + '@emnapi/runtime@1.7.0': + resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} - '@ianvs/prettier-plugin-sort-imports@4.7.0': - resolution: {integrity: sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==} - peerDependencies: - '@prettier/plugin-oxc': ^0.0.4 - '@vue/compiler-sfc': 2.7.x || 3.x - content-tag: ^4.0.0 - prettier: 2 || 3 || ^4.0.0-0 - prettier-plugin-ember-template-tag: ^2.1.0 + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' peerDependenciesMeta: - '@prettier/plugin-oxc': - optional: true - '@vue/compiler-sfc': - optional: true - content-tag: - optional: true - prettier-plugin-ember-template-tag: + '@types/react': optional: true - '@img/sharp-darwin-arm64@0.34.3': - resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} cpu: [arm64] - os: [darwin] + os: [android] - '@img/sharp-darwin-x64@0.34.3': - resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} cpu: [x64] - os: [darwin] + os: [android] - '@img/sharp-libvips-darwin-arm64@1.2.0': - resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.0': - resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.0': - resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.2.0': - resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-ppc64@1.2.0': - resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} - cpu: [ppc64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.2.0': - resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} - cpu: [s390x] - os: [linux] + os: [freebsd] - '@img/sharp-libvips-linux-x64@1.2.0': - resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} cpu: [x64] - os: [linux] + os: [freebsd] - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': - resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.0': - resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} - cpu: [x64] + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] os: [linux] - '@img/sharp-linux-arm64@0.34.3': - resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] os: [linux] - '@img/sharp-linux-arm@0.34.3': - resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] os: [linux] - '@img/sharp-linux-ppc64@0.34.3': - resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] os: [linux] - '@img/sharp-linux-s390x@0.34.3': - resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] os: [linux] - '@img/sharp-linux-x64@0.34.3': - resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.3': - resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.3': - resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.3': - resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.3': - resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.3': - resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] + os: [netbsd] - '@img/sharp-win32-x64@0.34.3': - resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} cpu: [x64] - os: [win32] + os: [netbsd] - '@inquirer/checkbox@4.1.5': - resolution: {integrity: sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==} + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + cpu: [arm64] + os: [openbsd] - '@inquirer/checkbox@4.2.0': - resolution: {integrity: sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==} + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + cpu: [x64] + os: [openbsd] - '@inquirer/confirm@5.1.14': - resolution: {integrity: sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==} + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + cpu: [arm64] + os: [openharmony] - '@inquirer/confirm@5.1.9': - resolution: {integrity: sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==} + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + cpu: [x64] + os: [sunos] - '@inquirer/core@10.1.10': - resolution: {integrity: sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==} + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + cpu: [arm64] + os: [win32] - '@inquirer/core@10.1.15': - resolution: {integrity: sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==} + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + cpu: [ia32] + os: [win32] - '@inquirer/editor@4.2.10': - resolution: {integrity: sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==} + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + cpu: [x64] + os: [win32] - '@inquirer/editor@4.2.15': - resolution: {integrity: sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==} - engines: {node: '>=18'} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@inquirer/expand@4.0.12': - resolution: {integrity: sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@inquirer/expand@4.0.17': - resolution: {integrity: sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==} - engines: {node: '>=18'} + '@eslint/compat@2.0.1': + resolution: {integrity: sha512-yl/JsgplclzuvGFNqwNYV4XNPhP3l62ZOP9w/47atNAdmDtIFCx6X7CSk/SlWUuBGkT4Et/5+UD+WyvX2iiIWA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: - '@types/node': '>=18' + eslint: ^8.40 || 9 peerDependenciesMeta: - '@types/node': + eslint: optional: true - '@inquirer/figures@1.0.11': - resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} - engines: {node: '>=18'} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@inquirer/figures@1.0.13': - resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} - engines: {node: '>=18'} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@inquirer/input@4.1.9': - resolution: {integrity: sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@inquirer/input@4.2.1': - resolution: {integrity: sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@eslint/core@1.0.1': + resolution: {integrity: sha512-r18fEAj9uCk+VjzGt2thsbOmychS+4kxI14spVNibUO2vqKX7obOG+ymZljAwuPZl+S3clPGwCwTDtrdqTiY6Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@inquirer/number@3.0.12': - resolution: {integrity: sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@inquirer/number@3.0.17': - resolution: {integrity: sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@inquirer/password@4.0.12': - resolution: {integrity: sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@inquirer/password@4.0.17': - resolution: {integrity: sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@inquirer/prompts@7.3.2': - resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@faker-js/faker@10.2.0': + resolution: {integrity: sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} - '@inquirer/prompts@7.8.0': - resolution: {integrity: sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} - '@inquirer/rawlist@4.0.12': - resolution: {integrity: sha512-wNPJZy8Oc7RyGISPxp9/MpTOqX8lr0r+lCCWm7hQra+MDtYRgINv1hxw7R+vKP71Bu/3LszabxOodfV/uTfsaA==} - engines: {node: '>=18'} + '@fast-csv/format@5.0.5': + resolution: {integrity: sha512-0P9SJXXnqKdmuWlLaTelqbrfdgN37Mvrb369J6eNmqL41IEIZQmV4sNM4GgAK2Dz3aH04J0HKGDMJFkYObThTw==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + + '@fast-csv/parse@5.0.5': + resolution: {integrity: sha512-M0IbaXZDbxfOnpVE5Kps/a6FGlILLhtLsvWd9qNH3d2TxNnpbNkFf3KD26OmJX6MHq7PdQAl5htStDwnuwHx6w==} + + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@fastify/cors@11.2.0': + resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} + + '@fastify/deepmerge@3.1.0': + resolution: {integrity: sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/formbody@8.0.2': + resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/multipart@9.4.0': + resolution: {integrity: sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@fastify/send@4.1.0': + resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} + + '@fastify/static@9.0.0': + resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hapi/address@5.1.1': + resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} + engines: {node: '>=14.0.0'} + + '@hapi/formula@3.0.2': + resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==} + + '@hapi/hoek@11.0.7': + resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/pinpoint@2.0.1': + resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} + + '@hapi/tlds@1.1.4': + resolution: {integrity: sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==} + engines: {node: '>=14.0.0'} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@hapi/topo@6.0.2': + resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@ianvs/prettier-plugin-sort-imports@4.7.0': + resolution: {integrity: sha512-soa2bPUJAFruLL4z/CnMfSEKGznm5ebz29fIa9PxYtu8HHyLKNE1NXAs6dylfw1jn/ilEIfO2oLLN6uAafb7DA==} + peerDependencies: + '@prettier/plugin-oxc': ^0.0.4 + '@vue/compiler-sfc': 2.7.x || 3.x + content-tag: ^4.0.0 + prettier: 2 || 3 || ^4.0.0-0 + prettier-plugin-ember-template-tag: ^2.1.0 + peerDependenciesMeta: + '@prettier/plugin-oxc': + optional: true + '@vue/compiler-sfc': + optional: true + content-tag: + optional: true + prettier-plugin-ember-template-tag: + optional: true + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/rawlist@4.1.5': - resolution: {integrity: sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==} + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2682,8 +3222,8 @@ packages: '@types/node': optional: true - '@inquirer/search@3.0.12': - resolution: {integrity: sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2691,8 +3231,8 @@ packages: '@types/node': optional: true - '@inquirer/search@3.1.0': - resolution: {integrity: sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==} + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2700,8 +3240,8 @@ packages: '@types/node': optional: true - '@inquirer/select@4.1.1': - resolution: {integrity: sha512-IUXzzTKVdiVNMA+2yUvPxWsSgOG4kfX93jOM4Zb5FgujeInotv5SPIJVeXQ+fO4xu7tW8VowFhdG5JRmmCyQ1Q==} + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2709,8 +3249,8 @@ packages: '@types/node': optional: true - '@inquirer/select@4.3.1': - resolution: {integrity: sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==} + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2718,8 +3258,8 @@ packages: '@types/node': optional: true - '@inquirer/type@3.0.6': - resolution: {integrity: sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==} + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2727,8 +3267,8 @@ packages: '@types/node': optional: true - '@inquirer/type@3.0.8': - resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -2756,12 +3296,12 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - '@jest/console@30.1.2': - resolution: {integrity: sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==} + '@jest/console@30.2.0': + resolution: {integrity: sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/core@30.1.3': - resolution: {integrity: sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==} + '@jest/core@30.2.0': + resolution: {integrity: sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==} 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 @@ -2769,16 +3309,16 @@ packages: node-notifier: optional: true - '@jest/create-cache-key-function@30.0.5': - resolution: {integrity: sha512-W1kmkwPq/WTMQWgvbzWSCbXSqvjI6rkqBQCxuvYmd+g6o4b5gHP98ikfh/Ei0SKzHvWdI84TOXp0hRcbpr8Q0w==} + '@jest/create-cache-key-function@30.2.0': + resolution: {integrity: sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/diff-sequences@30.0.1': resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/environment-jsdom-abstract@30.1.2': - resolution: {integrity: sha512-u8kTh/ZBl97GOmnGJLYK/1GuwAruMC4hoP6xuk/kwltmVWsA9u/6fH1/CsPVGt2O+Wn2yEjs8n1B1zZJ62Cx0w==} + '@jest/environment-jsdom-abstract@30.2.0': + resolution: {integrity: sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -2787,44 +3327,36 @@ packages: canvas: optional: true - '@jest/environment@30.1.2': - resolution: {integrity: sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/expect-utils@30.0.5': - resolution: {integrity: sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/expect-utils@30.1.2': - resolution: {integrity: sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==} + '@jest/environment@30.2.0': + resolution: {integrity: sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect@30.1.2': - resolution: {integrity: sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==} + '@jest/expect-utils@30.2.0': + resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/fake-timers@30.1.2': - resolution: {integrity: sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==} + '@jest/expect@30.2.0': + resolution: {integrity: sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/get-type@30.0.1': - resolution: {integrity: sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==} + '@jest/fake-timers@30.2.0': + resolution: {integrity: sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/get-type@30.1.0': resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/globals@30.1.2': - resolution: {integrity: sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==} + '@jest/globals@30.2.0': + resolution: {integrity: sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/pattern@30.0.1': resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/reporters@30.1.3': - resolution: {integrity: sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==} + '@jest/reporters@30.2.0': + resolution: {integrity: sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==} 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 @@ -2832,40 +3364,44 @@ packages: node-notifier: optional: true + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@30.0.5': resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/snapshot-utils@30.1.2': - resolution: {integrity: sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==} + '@jest/snapshot-utils@30.2.0': + resolution: {integrity: sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/source-map@30.0.1': resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-result@30.1.3': - resolution: {integrity: sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==} + '@jest/test-result@30.2.0': + resolution: {integrity: sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/test-sequencer@30.1.3': - resolution: {integrity: sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==} + '@jest/test-sequencer@30.2.0': + resolution: {integrity: sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/transform@30.1.2': - resolution: {integrity: sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==} + '@jest/transform@30.2.0': + resolution: {integrity: sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/types@30.0.5': - resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.12': - resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jest/types@30.2.0': + resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -2874,141 +3410,188 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@lukeed/csprng@1.1.0': - resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} - engines: {node: '>=8'} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' - '@lukeed/ms@2.0.2': - resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} - engines: {node: '>=8'} + '@jsonjoy.com/buffers@1.2.1': + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' - '@microsoft/tsdoc@0.15.1': - resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' - '@napi-rs/nice-android-arm-eabi@1.0.1': - resolution: {integrity: sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==} + '@jsonjoy.com/json-pack@1.21.0': + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@napi-rs/nice-android-arm-eabi@1.1.1': + resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} engines: {node: '>= 10'} cpu: [arm] os: [android] - '@napi-rs/nice-android-arm64@1.0.1': - resolution: {integrity: sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==} + '@napi-rs/nice-android-arm64@1.1.1': + resolution: {integrity: sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/nice-darwin-arm64@1.0.1': - resolution: {integrity: sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==} + '@napi-rs/nice-darwin-arm64@1.1.1': + resolution: {integrity: sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/nice-darwin-x64@1.0.1': - resolution: {integrity: sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==} + '@napi-rs/nice-darwin-x64@1.1.1': + resolution: {integrity: sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/nice-freebsd-x64@1.0.1': - resolution: {integrity: sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==} + '@napi-rs/nice-freebsd-x64@1.1.1': + resolution: {integrity: sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@napi-rs/nice-linux-arm-gnueabihf@1.0.1': - resolution: {integrity: sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==} + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/nice-linux-arm64-gnu@1.0.1': - resolution: {integrity: sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==} + '@napi-rs/nice-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/nice-linux-arm64-musl@1.0.1': - resolution: {integrity: sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==} + '@napi-rs/nice-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/nice-linux-ppc64-gnu@1.0.1': - resolution: {integrity: sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==} + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] - '@napi-rs/nice-linux-riscv64-gnu@1.0.1': - resolution: {integrity: sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==} + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': + resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@napi-rs/nice-linux-s390x-gnu@1.0.1': - resolution: {integrity: sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==} + '@napi-rs/nice-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] - '@napi-rs/nice-linux-x64-gnu@1.0.1': - resolution: {integrity: sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==} + '@napi-rs/nice-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/nice-linux-x64-musl@1.0.1': - resolution: {integrity: sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==} + '@napi-rs/nice-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/nice-win32-arm64-msvc@1.0.1': - resolution: {integrity: sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==} + '@napi-rs/nice-openharmony-arm64@1.1.1': + resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [openharmony] + + '@napi-rs/nice-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@napi-rs/nice-win32-ia32-msvc@1.0.1': - resolution: {integrity: sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==} + '@napi-rs/nice-win32-ia32-msvc@1.1.1': + resolution: {integrity: sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@napi-rs/nice-win32-x64-msvc@1.0.1': - resolution: {integrity: sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==} + '@napi-rs/nice-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/nice@1.0.1': - resolution: {integrity: sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==} + '@napi-rs/nice@1.1.1': + resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} engines: {node: '>= 10'} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.0.3': - resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} '@nestjs-modules/mailer@2.0.2': resolution: {integrity: sha512-+z4mADQasg0H1ZaGu4zZTuKv2pu+XdErqx99PLFPzCDNTN/q9U59WPgkxVaHnsvKHNopLj5Xap7G4ZpptduoYw==} @@ -3024,8 +3607,8 @@ packages: axios: ^1.3.1 rxjs: ^7.0.0 - '@nestjs/cli@11.0.10': - resolution: {integrity: sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==} + '@nestjs/cli@11.0.16': + resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: @@ -3037,8 +3620,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.6': - resolution: {integrity: sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==} + '@nestjs/common@11.1.12': + resolution: {integrity: sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -3056,8 +3639,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.1.6': - resolution: {integrity: sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==} + '@nestjs/core@11.1.12': + resolution: {integrity: sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -3080,8 +3663,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - '@nestjs/jwt@11.0.0': - resolution: {integrity: sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==} + '@nestjs/jwt@11.0.2': + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 @@ -3104,16 +3687,16 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - '@nestjs/platform-express@11.1.6': - resolution: {integrity: sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==} + '@nestjs/platform-express@11.1.12': + resolution: {integrity: sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/platform-fastify@11.1.6': - resolution: {integrity: sha512-udnIg7vfA103wppRkcMRVWX71S7NfeDnlprTndhcZzYXcDY2i5c+RwrQN/xU4Aw5X22Fg8ryi7bFbn6/Lquv8w==} + '@nestjs/platform-fastify@11.1.12': + resolution: {integrity: sha512-dX9g+/bzh3jqWuEf600bWkbONhVNLIyG95FamDPf7Uwukym99V2h/Qnl9hMaeNYZt83vrG7Ms3cxMib4Ar874A==} peerDependencies: - '@fastify/static': ^8.0.0 + '@fastify/static': ^8.0.0 || ^9.0.0 '@fastify/view': ^10.0.0 || ^11.0.0 '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3123,21 +3706,21 @@ packages: '@fastify/view': optional: true - '@nestjs/schedule@6.0.0': - resolution: {integrity: sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==} + '@nestjs/schedule@6.0.1': + resolution: {integrity: sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - '@nestjs/schematics@11.0.7': - resolution: {integrity: sha512-t8dNYYMwEeEsrlwc2jbkfwCfXczq4AeNEgx1KVQuJ6wYibXk0ZbXbPdfp8scnEAaQv1grpncNV5gWgzi7ZwbvQ==} + '@nestjs/schematics@11.0.9': + resolution: {integrity: sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==} peerDependencies: typescript: '>=4.8.2' - '@nestjs/swagger@11.2.0': - resolution: {integrity: sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==} + '@nestjs/swagger@11.2.5': + resolution: {integrity: sha512-wCykbEybMqiYcvkyzPW4SbXKcwra9AGdajm0MvFgKR3W+gd1hfeKlo67g/s9QCRc/mqUU4KOE5Qtk7asMeFuiA==} peerDependencies: - '@fastify/static': ^8.0.0 + '@fastify/static': ^8.0.0 || ^9.0.0 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 class-transformer: '*' @@ -3199,8 +3782,8 @@ packages: typeorm: optional: true - '@nestjs/testing@11.1.6': - resolution: {integrity: sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==} + '@nestjs/testing@11.1.12': + resolution: {integrity: sha512-W0M/i5nb9qRQpTQfJm+1mGT/+y4YezwwdcD7mxFG8JEZ5fz/ZEAk1Ayri2VBJKJUdo20B1ggnvqew4dlTMrSNg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3221,56 +3804,56 @@ packages: rxjs: ^7.2.0 typeorm: ^0.3.0 - '@next/env@15.4.7': - resolution: {integrity: sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==} + '@next/env@16.1.3': + resolution: {integrity: sha512-BLP14oBOvZWXgfdJf9ao+VD8O30uE+x7PaV++QtACLX329WcRSJRO5YJ+Bcvu0Q+c/lei41TjSiFf6pXqnpbQA==} - '@next/eslint-plugin-next@15.4.7': - resolution: {integrity: sha512-asj3RRiEruRLVr+k2ZC4hll9/XBzegMpFMr8IIRpNUYypG86m/a76339X2WETl1C53A512w2INOc2KZV769KPA==} + '@next/eslint-plugin-next@16.1.3': + resolution: {integrity: sha512-MqBh3ltFAy0AZCRFVdjVjjeV7nEszJDaVIpDAnkQcn8U9ib6OEwkSnuK6xdYxMGPhV/Y4IlY6RbDipPOpLfBqQ==} - '@next/swc-darwin-arm64@15.4.7': - resolution: {integrity: sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==} + '@next/swc-darwin-arm64@16.1.3': + resolution: {integrity: sha512-CpOD3lmig6VflihVoGxiR/l5Jkjfi4uLaOR4ziriMv0YMDoF6cclI+p5t2nstM8TmaFiY6PCTBgRWB57/+LiBA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.7': - resolution: {integrity: sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==} + '@next/swc-darwin-x64@16.1.3': + resolution: {integrity: sha512-aF4us2JXh0zn3hNxvL1Bx3BOuh8Lcw3p3Xnurlvca/iptrDH1BrpObwkw9WZra7L7/0qB9kjlREq3hN/4x4x+Q==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.7': - resolution: {integrity: sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==} + '@next/swc-linux-arm64-gnu@16.1.3': + resolution: {integrity: sha512-8VRkcpcfBtYvhGgXAF7U3MBx6+G1lACM1XCo1JyaUr4KmAkTNP8Dv2wdMq7BI+jqRBw3zQE7c57+lmp7jCFfKA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.7': - resolution: {integrity: sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==} + '@next/swc-linux-arm64-musl@16.1.3': + resolution: {integrity: sha512-UbFx69E2UP7MhzogJRMFvV9KdEn4sLGPicClwgqnLht2TEi204B71HuVfps3ymGAh0c44QRAF+ZmvZZhLLmhNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.7': - resolution: {integrity: sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==} + '@next/swc-linux-x64-gnu@16.1.3': + resolution: {integrity: sha512-SzGTfTjR5e9T+sZh5zXqG/oeRQufExxBF6MssXS7HPeZFE98JDhCRZXpSyCfWrWrYrzmnw/RVhlP2AxQm+wkRQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.7': - resolution: {integrity: sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==} + '@next/swc-linux-x64-musl@16.1.3': + resolution: {integrity: sha512-HlrDpj0v+JBIvQex1mXHq93Mht5qQmfyci+ZNwGClnAQldSfxI6h0Vupte1dSR4ueNv4q7qp5kTnmLOBIQnGow==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.7': - resolution: {integrity: sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==} + '@next/swc-win32-arm64-msvc@16.1.3': + resolution: {integrity: sha512-3gFCp83/LSduZMSIa+lBREP7+5e7FxpdBoc9QrCdmp+dapmTK9I+SLpY60Z39GDmTXSZA4huGg9WwmYbr6+WRw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.7': - resolution: {integrity: sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==} + '@next/swc-win32-x64-msvc@16.1.3': + resolution: {integrity: sha512-1SZVfFT8zmMB+Oblrh5OKDvUo5mYQOkX2We6VGzpg7JUVZlqe4DYOFGKYZKTweSx1gbMixyO1jnFT4thU+nNHQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3400,103 +3983,106 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@oxc-resolver/binding-android-arm-eabi@11.6.1': - resolution: {integrity: sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==} + '@oxc-resolver/binding-android-arm-eabi@11.13.1': + resolution: {integrity: sha512-YijiebZnGbKtwhLJXmUkOTS2iFF5Mh7TZb3SpVGrbgH6t2flJn7K+k78FJN7tc2lfixdlI1amkcCbTCgV+2WwQ==} cpu: [arm] os: [android] - '@oxc-resolver/binding-android-arm64@11.6.1': - resolution: {integrity: sha512-xjL/FKKc5p8JkFWiH7pJWSzsewif3fRf1rw2qiRxRvq1uIa6l7Zoa14Zq2TNWEsqDjdeOrlJtfWiPNRnevK0oQ==} + '@oxc-resolver/binding-android-arm64@11.13.1': + resolution: {integrity: sha512-cURsasEvObw/KCi8eRuZhHiT4agR4cui6uWX8ss2z/Ok23f8W+P8fvEZD0iUMIAmHwyAxA93RxNTIKh48zK39A==} cpu: [arm64] os: [android] - '@oxc-resolver/binding-darwin-arm64@11.6.1': - resolution: {integrity: sha512-u0yrJ3NHE0zyCjiYpIyz4Vmov21MA0yFKbhHgixDU/G6R6nvC8ZpuSFql3+7C8ttAK9p8WpqOGweepfcilH5Bw==} + '@oxc-resolver/binding-darwin-arm64@11.13.1': + resolution: {integrity: sha512-IKsn9oeVrbWpbE+PanGr5C4tRPVhVuBh/ZY8I7bbqaxBjemlgKKNGNSq73VDzQjRApJgjjzsVDgkTwTrKivLGg==} cpu: [arm64] os: [darwin] - '@oxc-resolver/binding-darwin-x64@11.6.1': - resolution: {integrity: sha512-2lox165h1EhzxcC8edUy0znXC/hnAbUPaMpYKVlzLpB2AoYmgU4/pmofFApj+axm2FXpNamjcppld8EoHo06rw==} + '@oxc-resolver/binding-darwin-x64@11.13.1': + resolution: {integrity: sha512-FW9toaDOXSLmP3lYXsXPalQKLs8eXwZCNUOPeng84MExl+ALe0Ik+sif/U6P/nqJgVdVm4MEiZcnnNtQ+Bn29Q==} cpu: [x64] os: [darwin] - '@oxc-resolver/binding-freebsd-x64@11.6.1': - resolution: {integrity: sha512-F45MhEQ7QbHfsvZtVNuA/9obu3il7QhpXYmCMfxn7Zt9nfAOw4pQ8hlS5DroHVp3rW35u9F7x0sixk/QEAi3qQ==} + '@oxc-resolver/binding-freebsd-x64@11.13.1': + resolution: {integrity: sha512-9EODydJ8P/DhEmVIdcjLnlDXAw9hot2NLuwY1/6gp3fKNXsqz3s9ch/vlDpq0CMtvjQ3Z4a2P+4IsH5A73Eh/A==} cpu: [x64] os: [freebsd] - '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.1': - resolution: {integrity: sha512-r+3+MTTl0tD4NoWbfTIItAxJvuyIU7V0fwPDXrv7Uj64vZ3OYaiyV+lVaeU89Bk/FUUQxeUpWBwdKNKHjyRNQw==} + '@oxc-resolver/binding-linux-arm-gnueabihf@11.13.1': + resolution: {integrity: sha512-Ud/q31NNEFXVy9mwO1jbXXsuqYd8ftoweL4z9MZ5wahlncnzPYKcEGSdBfSi7TKct4KU8EdvAxi+F9wdO1dCGw==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm-musleabihf@11.6.1': - resolution: {integrity: sha512-TBTZ63otsWZ72Z8ZNK2JVS0HW1w9zgOixJTFDNrYPUUW1pXGa28KAjQ1yGawj242WLAdu3lwdNIWtkxeO2BLxQ==} + '@oxc-resolver/binding-linux-arm-musleabihf@11.13.1': + resolution: {integrity: sha512-4x/eNAoQ7Ec2n81S2akaBeDbM4ceuy8R4sd41p1ETnM5PBhvBzWSuf75vQp4K1dLyKKPe+fw+uG4eIpgzqvj8A==} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm64-gnu@11.6.1': - resolution: {integrity: sha512-SjwhNynjSG2yMdyA0f7wz7Yvo3ppejO+ET7n2oiI7ApCXrwxMzeRWjBzQt+oVWr2HzVOfaEcDS9rMtnR83ulig==} + '@oxc-resolver/binding-linux-arm64-gnu@11.13.1': + resolution: {integrity: sha512-435Sf0a1KKjU7jgB5gcisTq6WMxQQVfsmKWAcQ3VhbXU/NpaUUZaezKmZJXNiAO1sUY6/zRJnTaPtsBq9msYlQ==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-arm64-musl@11.6.1': - resolution: {integrity: sha512-f4EMidK6rosInBzPMnJ0Ri4RttFCvvLNUNDFUBtELW/MFkBwPTDlvbsmW0u0Mk/ruBQ2WmRfOZ6tT62kWMcX2Q==} + '@oxc-resolver/binding-linux-arm64-musl@11.13.1': + resolution: {integrity: sha512-Okb7KgPJvA/Db0QwdVziuYs5MZQEq9PC5MEDrBK7jmcqQL2RO+mk7oztqSegcNJ7kMyNM7Zi2cN9G69g4Cs3zg==} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-ppc64-gnu@11.6.1': - resolution: {integrity: sha512-1umENVKeUsrWnf5IlF/6SM7DCv8G6CoKI2LnYR6qhZuLYDPS4PBZ0Jow3UDV9Rtbv5KRPcA3/uXjI88ntWIcOQ==} + '@oxc-resolver/binding-linux-ppc64-gnu@11.13.1': + resolution: {integrity: sha512-HyM9+MlH7bWQtjtGzhxVMVhIuy2C1+MqavBfSMyY2d9SSdxcKvboMhl/0vTTMH/R94z8n/gP5XSJ1M6/BC30Pw==} cpu: [ppc64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-gnu@11.6.1': - resolution: {integrity: sha512-Hjyp1FRdJhsEpIxsZq5VcDuFc8abC0Bgy8DWEa31trCKoTz7JqA7x3E2dkFbrAKsEFmZZ0NvuG5Ip3oIRARhow==} + '@oxc-resolver/binding-linux-riscv64-gnu@11.13.1': + resolution: {integrity: sha512-ukJFu+798IzODSIupFAbouehJOLqQwhz56VlzRXi+42xtsmtZ+NLla2CXlaw1V9nMB7HLEQU1+XklkeFsIxz4g==} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-musl@11.6.1': - resolution: {integrity: sha512-ODJOJng6f3QxpAXhLel3kyWs8rPsJeo9XIZHzA7p//e+5kLMDU7bTVk4eZnUHuxsqsB8MEvPCicJkKCEuur5Ag==} + '@oxc-resolver/binding-linux-riscv64-musl@11.13.1': + resolution: {integrity: sha512-gCr05/1CbuKQ/E39pzVjBLE/amtdvFpHeEd6lUOshnoInZ48g33b+1/CNyeO+B1CoiIydYGrkbyIoIeSMWzSsw==} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-s390x-gnu@11.6.1': - resolution: {integrity: sha512-hCzRiLhqe1ZOpHTsTGKp7gnMJRORlbCthawBueer2u22RVAka74pV/+4pP1tqM07mSlQn7VATuWaDw9gCl+cVg==} + '@oxc-resolver/binding-linux-s390x-gnu@11.13.1': + resolution: {integrity: sha512-ojQVasxjsZGCxt+ygyipCSp74P22WdUToBLM8D9qVm/yehOtxIT8nv0FyQrc4DOpqzGPxQS2OcgvLag+9AhsFg==} cpu: [s390x] os: [linux] - '@oxc-resolver/binding-linux-x64-gnu@11.6.1': - resolution: {integrity: sha512-JansPD8ftOzMYIC3NfXJ68tt63LEcIAx44Blx6BAd7eY880KX7A0KN3hluCrelCz5aQkPaD95g8HBiJmKaEi2w==} + '@oxc-resolver/binding-linux-x64-gnu@11.13.1': + resolution: {integrity: sha512-Vr28gTydAegrq+qmQu4IvR+LEq3A8amuHdOPSOwMM44cwpIvEDd4MmhimfEqoWjcfVZy9vpd5mPZZY6C/lHq9g==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-linux-x64-musl@11.6.1': - resolution: {integrity: sha512-R78ES1rd4z2x5NrFPtSWb/ViR1B8wdl+QN2X8DdtoYcqZE/4tvWtn9ZTCXMEzUp23tchJ2wUB+p6hXoonkyLpA==} + '@oxc-resolver/binding-linux-x64-musl@11.13.1': + resolution: {integrity: sha512-a2g2nv3IulLb9lHd8ZDGEnWIpNXcZviLiEKt+PHP3k3d86U1adlL5rNmImjF+eNGReTyttlX/hYNT4UIPo7IjA==} cpu: [x64] os: [linux] - '@oxc-resolver/binding-wasm32-wasi@11.6.1': - resolution: {integrity: sha512-qAR3tYIf3afkij/XYunZtlz3OH2Y4ni10etmCFIJB5VRGsqJyI6Hl+2dXHHGJNwbwjXjSEH/KWJBpVroF3TxBw==} + '@oxc-resolver/binding-wasm32-wasi@11.13.1': + resolution: {integrity: sha512-PhvfJQG6IyI9uN1c5NAZqfl1N9lLF1XdenX+H3aHYHlADPiOgwtpQgBETSD2L3ySeR7jLzJRVFUrWEu4uDz7Lg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-resolver/binding-win32-arm64-msvc@11.6.1': - resolution: {integrity: sha512-QqygWygIuemGkaBA48POOTeinbVvlamqh6ucm8arGDGz/mB5O00gXWxed12/uVrYEjeqbMkla/CuL3fjL3EKvw==} + '@oxc-resolver/binding-win32-arm64-msvc@11.13.1': + resolution: {integrity: sha512-hyKUC0JQbTKoaPw3r9XHWHtj+B/win36VjTyKDd0OjG71UeyAhZiJBjoNJwfmnTIPcQS4YNesjNkqqDe4qN44w==} cpu: [arm64] os: [win32] - '@oxc-resolver/binding-win32-ia32-msvc@11.6.1': - resolution: {integrity: sha512-N2+kkWwt/bk0JTCxhPuK8t8JMp3nd0n2OhwOkU8KO4a7roAJEa4K1SZVjMv5CqUIr5sx2CxtXRBoFDiORX5oBg==} + '@oxc-resolver/binding-win32-ia32-msvc@11.13.1': + resolution: {integrity: sha512-0/y+YMQJEd8kltqPTAUi1PHsYTUi/7UL8Jkhh6BODn3VBQIMMfHhyS8MH4geYJLEJUxuRxGKtya57GOTAN2WSw==} cpu: [ia32] os: [win32] - '@oxc-resolver/binding-win32-x64-msvc@11.6.1': - resolution: {integrity: sha512-DfMg3cU9bJUbN62Prbp4fGCtLgexuwyEaQGtZAp8xmi1Ii26uflOGx0FJkFTF6lVMSFoIRFvIL8gsw5/ZdHrMw==} + '@oxc-resolver/binding-win32-x64-msvc@11.13.1': + resolution: {integrity: sha512-0r1P/PDUD936rZShGdfnqNFdozRVgFYrcdajm1ZZ8wMoN594YkjKmlM3z3DB6arS+Bz7RhA9uLXcP74GqZ/lAw==} cpu: [x64] os: [win32] - '@paralleldrive/cuid2@2.2.2': - resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -3506,11 +4092,26 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.55.0': - resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==} + '@playwright/test@1.57.0': + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} engines: {node: '>=18'} hasBin: true + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -3679,8 +4280,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-label@2.1.7': - resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -3770,8 +4371,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.2': - resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -3783,8 +4384,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -3848,8 +4449,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.7': - resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -3874,8 +4475,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.2.2': - resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==} + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -3883,8 +4484,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -4058,8 +4659,8 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@redocly/ajv@8.11.2': - resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + '@redocly/ajv@8.11.4': + resolution: {integrity: sha512-77MhyFgZ1zGMwtCpqsk532SJEc3IJmSOXKTCeWoMTAvPnQOkuOgxEip1n5pG5YX1IzCTJ4kCvPKr8xYyzWFdhg==} '@redocly/config@0.22.2': resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} @@ -4068,13 +4669,24 @@ packages: resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==} engines: {node: '>=18.17.0', npm: '>=9.5.0'} - '@remixicon/react@4.6.0': - resolution: {integrity: sha512-bY56maEgT5IYUSRotqy9h03IAKJC85vlKtWFg2FKzfs8JPrkdBAYSa9dxoUSKFwGzup8Ux6vjShs9Aec3jvr2w==} + '@reduxjs/toolkit@2.10.1': + resolution: {integrity: sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@remixicon/react@4.8.0': + resolution: {integrity: sha512-cbzR04GKWa3zWdgn0C2i+u/avb167iWeu9gqFO00UGu84meARPAm3oKowDZTU6dlk/WS3UHo6k//LMRM1l7CRw==} peerDependencies: react: '>=18.2.0' - '@rollup/plugin-commonjs@28.0.6': - resolution: {integrity: sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==} + '@rollup/plugin-commonjs@29.0.0': + resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==} engines: {node: '>=16.0.0 || 14 >= 14.17'} peerDependencies: rollup: ^2.68.0||^3.0.0||^4.0.0 @@ -4082,8 +4694,8 @@ packages: rollup: optional: true - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -4091,98 +4703,113 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.34.8': - resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==} + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.8': - resolution: {integrity: sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==} + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.8': - resolution: {integrity: sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==} + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.8': - resolution: {integrity: sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==} + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.8': - resolution: {integrity: sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==} + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.34.8': - resolution: {integrity: sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==} + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.8': - resolution: {integrity: sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.34.8': - resolution: {integrity: sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==} + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.34.8': - resolution: {integrity: sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==} + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.34.8': - resolution: {integrity: sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==} + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.34.8': - resolution: {integrity: sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==} + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': - resolution: {integrity: sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==} + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.34.8': - resolution: {integrity: sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==} + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.34.8': - resolution: {integrity: sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==} + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.34.8': - resolution: {integrity: sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==} + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.34.8': - resolution: {integrity: sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==} + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.34.8': - resolution: {integrity: sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==} + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.34.8': - resolution: {integrity: sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==} + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.8': - resolution: {integrity: sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==} + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} cpu: [x64] os: [win32] @@ -4192,14 +4819,27 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@sinclair/typebox@0.34.38': - resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==} + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinclair/typebox@0.34.41': + resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} '@sindresorhus/is@5.6.0': resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} @@ -4211,336 +4851,229 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} - '@smithy/abort-controller@4.0.5': - resolution: {integrity: sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==} - engines: {node: '>=18.0.0'} - - '@smithy/abort-controller@4.1.0': - resolution: {integrity: sha512-wEhSYznxOmx7EdwK1tYEWJF5+/wmSFsff9BfTOn8oO/+KPl3gsmThrb6MJlWbOC391+Ya31s5JuHiC2RlT80Zg==} - engines: {node: '>=18.0.0'} - - '@smithy/chunked-blob-reader-native@4.0.0': - resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==} - engines: {node: '>=18.0.0'} - - '@smithy/chunked-blob-reader@5.0.0': - resolution: {integrity: sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==} - engines: {node: '>=18.0.0'} + '@slorber/react-helmet-async@1.3.0': + resolution: {integrity: sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@smithy/config-resolver@4.1.5': - resolution: {integrity: sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==} - engines: {node: '>=18.0.0'} + '@slorber/remark-comment@1.0.0': + resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} - '@smithy/config-resolver@4.2.0': - resolution: {integrity: sha512-FA10YhPFLy23uxeWu7pOM2ctlw+gzbPMTZQwrZ8FRIfyJ/p8YIVz7AVTB5jjLD+QIerydyKcVMZur8qzzDILAQ==} + '@smithy/abort-controller@4.2.8': + resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} engines: {node: '>=18.0.0'} - '@smithy/core@3.10.0': - resolution: {integrity: sha512-bXyD3Ij6b1qDymEYlEcF+QIjwb9gObwZNaRjETJsUEvSIzxFdynSQ3E4ysY7lUFSBzeWBNaFvX+5A0smbC2q6A==} + '@smithy/chunked-blob-reader-native@4.2.1': + resolution: {integrity: sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.8.0': - resolution: {integrity: sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==} + '@smithy/chunked-blob-reader@5.2.0': + resolution: {integrity: sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.0.7': - resolution: {integrity: sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==} + '@smithy/config-resolver@4.4.6': + resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.1.0': - resolution: {integrity: sha512-iVwNhxTsCQTPdp++4C/d9xvaDmuEWhXi55qJobMp9QMaEHRGH3kErU4F8gohtdsawRqnUy/ANylCjKuhcR2mPw==} + '@smithy/core@3.20.6': + resolution: {integrity: sha512-BpAffW1mIyRZongoKBbh3RgHG+JDHJek/8hjA/9LnPunM+ejorO6axkxCgwxCe4K//g/JdPeR9vROHDYr/hfnQ==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.0.5': - resolution: {integrity: sha512-miEUN+nz2UTNoRYRhRqVTJCx7jMeILdAurStT2XoS+mhokkmz1xAPp95DFW9Gxt4iF2VBqpeF9HbTQ3kY1viOA==} + '@smithy/credential-provider-imds@4.2.8': + resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.0.5': - resolution: {integrity: sha512-LCUQUVTbM6HFKzImYlSB9w4xafZmpdmZsOh9rIl7riPC3osCgGFVP+wwvYVw6pXda9PPT9TcEZxaq3XE81EdJQ==} + '@smithy/eventstream-codec@4.2.8': + resolution: {integrity: sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-config-resolver@4.1.3': - resolution: {integrity: sha512-yTTzw2jZjn/MbHu1pURbHdpjGbCuMHWncNBpJnQAPxOVnFUAbSIUSwafiphVDjNV93TdBJWmeVAds7yl5QCkcA==} + '@smithy/eventstream-serde-browser@4.2.8': + resolution: {integrity: sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-node@4.0.5': - resolution: {integrity: sha512-lGS10urI4CNzz6YlTe5EYG0YOpsSp3ra8MXyco4aqSkQDuyZPIw2hcaxDU82OUVtK7UY9hrSvgWtpsW5D4rb4g==} + '@smithy/eventstream-serde-config-resolver@4.3.8': + resolution: {integrity: sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.0.5': - resolution: {integrity: sha512-JFnmu4SU36YYw3DIBVao3FsJh4Uw65vVDIqlWT4LzR6gXA0F3KP0IXFKKJrhaVzCBhAuMsrUUaT5I+/4ZhF7aw==} + '@smithy/eventstream-serde-node@4.2.8': + resolution: {integrity: sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.1.1': - resolution: {integrity: sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==} + '@smithy/eventstream-serde-universal@4.2.8': + resolution: {integrity: sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.2.0': - resolution: {integrity: sha512-VZenjDdVaUGiy3hwQtxm75nhXZrhFG+3xyL93qCQAlYDyhT/jeDWM8/3r5uCFMlTmmyrIjiDyiOynVFchb0BSg==} + '@smithy/fetch-http-handler@5.3.9': + resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} engines: {node: '>=18.0.0'} - '@smithy/hash-blob-browser@4.0.5': - resolution: {integrity: sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==} + '@smithy/hash-blob-browser@4.2.9': + resolution: {integrity: sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==} engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.0.5': - resolution: {integrity: sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==} + '@smithy/hash-node@4.2.8': + resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} engines: {node: '>=18.0.0'} - '@smithy/hash-stream-node@4.0.5': - resolution: {integrity: sha512-IJuDS3+VfWB67UC0GU0uYBG/TA30w+PlOaSo0GPm9UHS88A6rCP6uZxNjNYiyRtOcjv7TXn/60cW8ox1yuZsLg==} + '@smithy/hash-stream-node@4.2.8': + resolution: {integrity: sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==} engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.0.5': - resolution: {integrity: sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==} + '@smithy/invalid-dependency@4.2.8': + resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/is-array-buffer@4.1.0': - resolution: {integrity: sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==} - engines: {node: '>=18.0.0'} - - '@smithy/md5-js@4.0.5': - resolution: {integrity: sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-content-length@4.0.5': - resolution: {integrity: sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.1.18': - resolution: {integrity: sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.2.0': - resolution: {integrity: sha512-J1eCF7pPDwgv7fGwRd2+Y+H9hlIolF3OZ2PjptonzzyOXXGh/1KGJAHpEcY1EX+WLlclKu2yC5k+9jWXdUG4YQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.1.19': - resolution: {integrity: sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.2.0': - resolution: {integrity: sha512-raL5oWYf5ALl3jCJrajE8enKJEnV/2wZkKS6mb3ZRY2tg3nj66ssdWy5Ps8E6Yu8Wqh3Tt+Sb9LozjvwZupq+A==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.0.9': - resolution: {integrity: sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.1.0': - resolution: {integrity: sha512-CtLFYlHt7c2VcztyVRc+25JLV4aGpmaSv9F1sPB0AGFL6S+RPythkqpGDa2XBQLJQooKkjLA1g7Xe4450knShg==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.0.5': - resolution: {integrity: sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.1.0': - resolution: {integrity: sha512-91Fuw4IKp0eK8PNhMXrHRcYA1jvbZ9BJGT91wwPy3bTQT8mHTcQNius/EhSQTlT9QUI3Ki1wjHeNXbWK0tO8YQ==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.1.4': - resolution: {integrity: sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.2.0': - resolution: {integrity: sha512-8/fpilqKurQ+f8nFvoFkJ0lrymoMJ+5/CQV5IcTv/MyKhk2Q/EFYCAgTSWHD4nMi9ux9NyBBynkyE9SLg2uSLA==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.1.1': - resolution: {integrity: sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.2.0': - resolution: {integrity: sha512-G4NV70B4hF9vBrUkkvNfWO6+QR4jYjeO4tc+4XrKCb4nPYj49V9Hu8Ftio7Mb0/0IlFyEOORudHrm+isY29nCA==} - engines: {node: '>=18.0.0'} - - '@smithy/property-provider@4.0.5': - resolution: {integrity: sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==} + '@smithy/is-array-buffer@4.2.0': + resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.1.0': - resolution: {integrity: sha512-eksMjMHUlG5PwOUWO3k+rfLNOPVPJ70mUzyYNKb5lvyIuAwS4zpWGsxGiuT74DFWonW0xRNy+jgzGauUzX7SyA==} + '@smithy/md5-js@4.2.8': + resolution: {integrity: sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.1.3': - resolution: {integrity: sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==} + '@smithy/middleware-content-length@4.2.8': + resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.2.0': - resolution: {integrity: sha512-bwjlh5JwdOQnA01be+5UvHK4HQz4iaRKlVG46hHSJuqi0Ribt3K06Z1oQ29i35Np4G9MCDgkOGcHVyLMreMcbg==} + '@smithy/middleware-endpoint@4.4.7': + resolution: {integrity: sha512-SCmhUG1UwtnEhF5Sxd8qk7bJwkj1BpFzFlHkXqKCEmDPLrRjJyTGM0EhqT7XBtDaDJjCfjRJQodgZcKDR843qg==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.0.5': - resolution: {integrity: sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==} + '@smithy/middleware-retry@4.4.23': + resolution: {integrity: sha512-lLEmkQj7I7oKfvZ1wsnToGJouLOtfkMXDKRA1Hi6F+mMp5O1N8GcVWmVeNgTtgZtd0OTXDTI2vpVQmeutydGew==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.1.0': - resolution: {integrity: sha512-JqTWmVIq4AF8R8OK/2cCCiQo5ZJ0SRPsDkDgLO5/3z8xxuUp1oMIBBjfuueEe+11hGTZ6rRebzYikpKc6yQV9Q==} + '@smithy/middleware-serde@4.2.9': + resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.0.5': - resolution: {integrity: sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==} + '@smithy/middleware-stack@4.2.8': + resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.1.0': - resolution: {integrity: sha512-VgdHhr8YTRsjOl4hnKFm7xEMOCRTnKw3FJ1nU+dlWNhdt/7eEtxtkdrJdx7PlRTabdANTmvyjE4umUl9cK4awg==} + '@smithy/node-config-provider@4.3.8': + resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.0.7': - resolution: {integrity: sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==} + '@smithy/node-http-handler@4.4.8': + resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.1.0': - resolution: {integrity: sha512-UBpNFzBNmS20jJomuYn++Y+soF8rOK9AvIGjS9yGP6uRXF5rP18h4FDUsoNpWTlSsmiJ87e2DpZo9ywzSMH7PQ==} + '@smithy/property-provider@4.2.8': + resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.0.5': - resolution: {integrity: sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==} + '@smithy/protocol-http@5.3.8': + resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.1.0': - resolution: {integrity: sha512-W0VMlz9yGdQ/0ZAgWICFjFHTVU0YSfGoCVpKaExRM/FDkTeP/yz8OKvjtGjs6oFokCRm0srgj/g4Cg0xuHu8Rw==} + '@smithy/querystring-builder@4.2.8': + resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.1.3': - resolution: {integrity: sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==} + '@smithy/querystring-parser@4.2.8': + resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.4.10': - resolution: {integrity: sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==} + '@smithy/service-error-classification@4.2.8': + resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.6.0': - resolution: {integrity: sha512-TvlIshqx5PIi0I0AiR+PluCpJ8olVG++xbYkAIGCUkByaMUlfOXLgjQTmYbr46k4wuDe8eHiTIlUflnjK2drPQ==} + '@smithy/shared-ini-file-loader@4.4.3': + resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} engines: {node: '>=18.0.0'} - '@smithy/types@4.3.2': - resolution: {integrity: sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==} + '@smithy/signature-v4@5.3.8': + resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} engines: {node: '>=18.0.0'} - '@smithy/types@4.4.0': - resolution: {integrity: sha512-4jY91NgZz+ZnSFcVzWwngOW6VuK3gR/ihTwSU1R/0NENe9Jd8SfWgbhDCAGUWL3bI7DiDSW7XF6Ui6bBBjrqXw==} + '@smithy/smithy-client@4.10.8': + resolution: {integrity: sha512-wcr3UEL26k7lLoyf9eVDZoD1nNY3Fa1gbNuOXvfxvVWLGkOVW+RYZgUUp/bXHryJfycIOQnBq9o1JAE00ax8HQ==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.0.5': - resolution: {integrity: sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==} + '@smithy/types@4.12.0': + resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.1.0': - resolution: {integrity: sha512-/LYEIOuO5B2u++tKr1NxNxhZTrr3A63jW8N73YTwVeUyAlbB/YM+hkftsvtKAcMt3ADYo0FsF1GY3anehffSVQ==} + '@smithy/url-parser@4.2.8': + resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} engines: {node: '>=18.0.0'} - '@smithy/util-base64@4.0.0': - resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} + '@smithy/util-base64@4.3.0': + resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} engines: {node: '>=18.0.0'} - '@smithy/util-base64@4.1.0': - resolution: {integrity: sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==} + '@smithy/util-body-length-browser@4.2.0': + resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} engines: {node: '>=18.0.0'} - '@smithy/util-body-length-browser@4.0.0': - resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.1.0': - resolution: {integrity: sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.0.0': - resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} + '@smithy/util-body-length-node@4.2.1': + resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} engines: {node: '>=18.0.0'} '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} - '@smithy/util-buffer-from@4.0.0': - resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} - engines: {node: '>=18.0.0'} - - '@smithy/util-buffer-from@4.1.0': - resolution: {integrity: sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.0.0': - resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.1.0': - resolution: {integrity: sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.0.26': - resolution: {integrity: sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.1.0': - resolution: {integrity: sha512-D27cLtJtC4EEeERJXS+JPoogz2tE5zeE3zhWSSu6ER5/wJ5gihUxIzoarDX6K1U27IFTHit5YfHqU4Y9RSGE0w==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.0.26': - resolution: {integrity: sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==} + '@smithy/util-buffer-from@4.2.0': + resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.1.0': - resolution: {integrity: sha512-gnZo3u5dP1o87plKupg39alsbeIY1oFFnCyV2nI/++pL19vTtBLgOyftLEjPjuXmoKn2B2rskX8b7wtC/+3Okg==} + '@smithy/util-config-provider@4.2.0': + resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.0.7': - resolution: {integrity: sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==} + '@smithy/util-defaults-mode-browser@4.3.22': + resolution: {integrity: sha512-O2WXr6ZRqPnbyoepb7pKcLt1QL6uRfFzGYJ9sGb5hMJQi7v/4RjRmCQa9mNjA0YiXqsc5lBmLXqJPhjM1Vjv5A==} engines: {node: '>=18.0.0'} - '@smithy/util-hex-encoding@4.1.0': - resolution: {integrity: sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==} + '@smithy/util-defaults-mode-node@4.2.25': + resolution: {integrity: sha512-7uMhppVNRbgNIpyUBVRfjGHxygP85wpXalRvn9DvUlCx4qgy1AB/uxOPSiDx/jFyrwD3/BypQhx1JK7f3yxrAw==} engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.0.5': - resolution: {integrity: sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==} + '@smithy/util-endpoints@3.2.8': + resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.1.0': - resolution: {integrity: sha512-612onNcKyxhP7/YOTKFTb2F6sPYtMRddlT5mZvYf1zduzaGzkYhpYIPxIeeEwBZFjnvEqe53Ijl2cYEfJ9d6/Q==} + '@smithy/util-hex-encoding@4.2.0': + resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.0.7': - resolution: {integrity: sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==} + '@smithy/util-middleware@4.2.8': + resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.1.0': - resolution: {integrity: sha512-5AGoBHb207xAKSVwaUnaER+L55WFY8o2RhlafELZR3mB0J91fpL+Qn+zgRkPzns3kccGaF2vy0HmNVBMWmN6dA==} + '@smithy/util-retry@4.2.8': + resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.3.0': - resolution: {integrity: sha512-ZOYS94jksDwvsCJtppHprUhsIscRnCKGr6FXCo3SxgQ31ECbza3wqDBqSy6IsAak+h/oAXb1+UYEBmDdseAjUQ==} + '@smithy/util-stream@4.5.10': + resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==} engines: {node: '>=18.0.0'} - '@smithy/util-uri-escape@4.0.0': - resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.1.0': - resolution: {integrity: sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==} + '@smithy/util-uri-escape@4.2.0': + resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} engines: {node: '>=18.0.0'} '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@smithy/util-utf8@4.0.0': - resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} + '@smithy/util-utf8@4.2.0': + resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} engines: {node: '>=18.0.0'} - '@smithy/util-utf8@4.1.0': - resolution: {integrity: sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==} + '@smithy/util-waiter@4.2.8': + resolution: {integrity: sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==} engines: {node: '>=18.0.0'} - '@smithy/util-waiter@4.0.7': - resolution: {integrity: sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==} + '@smithy/uuid@1.1.0': + resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} '@sqltools/formatter@1.2.5': @@ -4653,8 +5186,8 @@ packages: '@swc-node/sourcemap-support@0.6.1': resolution: {integrity: sha512-ovltDVH5QpdHXZkW138vG4+dgcNsxfwxHVoV6BtmTbz2KKl1A8ZSlbdtxzzfNjCjbpayda8Us9eMtcHobm38dA==} - '@swc/cli@0.7.8': - resolution: {integrity: sha512-27Ov4rm0s2C6LLX+NDXfDVB69LGs8K94sXtFhgeUyQ4DBywZuCgTBu2loCNHRr8JhT9DeQvJM5j9FAu/THbo4w==} + '@swc/cli@0.7.10': + resolution: {integrity: sha512-QQ36Q1VwGTT2YzvMeNe/j1x4DKS277DscNhWc57dIwQn//C+zAgvuSupMB/XkmYqPKQX+8hjn5/cHRJrMvWy0Q==} engines: {node: '>= 16.14.0'} hasBin: true peerDependencies: @@ -4739,8 +5272,8 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@swc/helpers@0.5.17': - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} '@swc/jest@0.2.39': resolution: {integrity: sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==} @@ -4748,20 +5281,20 @@ packages: peerDependencies: '@swc/core': '*' - '@swc/types@0.1.24': - resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} '@szmarczak/http-timer@5.0.1': resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@t3-oss/env-core@0.13.8': - resolution: {integrity: sha512-L1inmpzLQyYu4+Q1DyrXsGJYCXbtXjC4cICw1uAKv0ppYPQv656lhZPU91Qd1VS6SO/bou1/q5ufVzBGbNsUpw==} + '@t3-oss/env-core@0.13.10': + resolution: {integrity: sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g==} peerDependencies: arktype: ^2.1.0 typescript: '>=5.0.0' valibot: ^1.0.0-beta.7 || ^1.0.0 - zod: ^3.24.0 || ^4.0.0-beta.0 + zod: ^3.24.0 || ^4.0.0 peerDependenciesMeta: arktype: optional: true @@ -4772,13 +5305,13 @@ packages: zod: optional: true - '@t3-oss/env-nextjs@0.13.8': - resolution: {integrity: sha512-QmTLnsdQJ8BiQad2W2nvV6oUpH4oMZMqnFEjhVpzU0h3sI9hn8zb8crjWJ1Amq453mGZs6A4v4ihIeBFDOrLeQ==} + '@t3-oss/env-nextjs@0.13.10': + resolution: {integrity: sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ==} peerDependencies: arktype: ^2.1.0 typescript: '>=5.0.0' valibot: ^1.0.0-beta.7 || ^1.0.0 - zod: ^3.24.0 || ^4.0.0-beta.0 + zod: ^3.24.0 || ^4.0.0 peerDependenciesMeta: arktype: optional: true @@ -4789,20 +5322,20 @@ packages: zod: optional: true - '@tanstack/query-core@5.87.1': - resolution: {integrity: sha512-HOFHVvhOCprrWvtccSzc7+RNqpnLlZ5R6lTmngb8aq7b4rc2/jDT0w+vLdQ4lD9bNtQ+/A4GsFXy030Gk4ollA==} + '@tanstack/query-core@5.90.19': + resolution: {integrity: sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA==} - '@tanstack/query-devtools@5.86.0': - resolution: {integrity: sha512-/JDw9BP80eambEK/EsDMGAcsL2VFT+8F5KCOwierjPU7QP8Wt1GT32yJpn3qOinBM8/zS3Jy36+F0GiyJp411A==} + '@tanstack/query-devtools@5.92.0': + resolution: {integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==} - '@tanstack/react-query-devtools@5.87.1': - resolution: {integrity: sha512-YPuEub8RQrrsXOxoiMJn33VcGPIeuVINWBgLu9RLSQB8ueXaKlGLZ3NJkahGpbt2AbWf749FQ6R+1jBFk3kdCA==} + '@tanstack/react-query-devtools@5.91.2': + resolution: {integrity: sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==} peerDependencies: - '@tanstack/react-query': ^5.87.1 + '@tanstack/react-query': ^5.90.14 react: ^18 || ^19 - '@tanstack/react-query@5.87.1': - resolution: {integrity: sha512-YKauf8jfMowgAqcxj96AHs+Ux3m3bWT1oSVKamaRPXSnW2HqSznnTCEkAVqctF1e/W9R/mPcyzzINIgpOH94qg==} + '@tanstack/react-query@5.90.19': + resolution: {integrity: sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ==} peerDependencies: react: ^18 || ^19 @@ -4817,16 +5350,16 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@testing-library/dom@10.3.2': - resolution: {integrity: sha512-0bxIdP9mmPiOJ6wHLj8bdJRq+51oddObeCGdEf6PNEhYd93ZYAN+lPRnEOVFtheVwDM7+p+tza3LAQgp0PTudg==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.8.0': - resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -4850,6 +5383,10 @@ packages: resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -4875,8 +5412,8 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tybys/wasm-util@0.10.0': - resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -4884,23 +5421,29 @@ packages: '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} '@types/bcrypt@6.0.0': resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} - '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/cls-hooked@4.3.8': - resolution: {integrity: sha512-tf/7H883gFA6MPlWI15EQtfNZ+oPL0gLKkOlx9UHFrun1fC/FkuyNBpTKq1B5E3T4fbvjId6WifHUdSGsMMuPg==} + '@types/bonjour@3.5.13': + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + + '@types/cls-hooked@4.3.9': + resolution: {integrity: sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==} + + '@types/connect-history-api-fallback@1.5.4': + resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -4908,8 +5451,8 @@ packages: '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - '@types/d3-array@3.2.1': - resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} @@ -4920,14 +5463,14 @@ packages: '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - '@types/d3-path@3.1.0': - resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - '@types/d3-scale@4.0.8': - resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - '@types/d3-shape@3.1.6': - resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} '@types/d3-time@3.0.4': resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} @@ -4935,6 +5478,9 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/ejs@3.1.5': resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} @@ -4944,26 +5490,49 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@5.0.6': - resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} - '@types/express@5.0.3': - resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - '@types/hoist-non-react-statics@3.3.6': - resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/gtag.js@0.0.12': + resolution: {integrity: sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + + '@types/html-minifier-terser@6.1.0': + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/http-proxy@1.17.17': + resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -4989,38 +5558,47 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@9.0.5': - resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} - - '@types/jsonwebtoken@9.0.7': - resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} - - '@types/luxon@3.6.2': - resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} '@types/luxon@3.7.1': resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/mjml-core@4.7.4': - resolution: {integrity: sha512-hajbYITLm/wJU99Of50Dmn/k4ok+mrhJs4qDdnveJsINdiNJhQd+03C6Kt09vF9biB23cEI4pPeLrJNYfIZf7g==} + '@types/mjml-core@4.15.2': + resolution: {integrity: sha512-Q7SxFXgoX979HP57DEVsRI50TV8x1V4lfCA4Up9AvfINDM5oD/X9ARgfoyX1qS987JCnDLv85JjkqAjt3hZSiQ==} '@types/mjml@4.7.4': resolution: {integrity: sha512-vyi1vzWgMzFMwZY7GSZYX0GU0dmtC8vLHwpgk+NWmwbwRSrlieVyJ9sn5elodwUfklJM7yGl0zQeet1brKTWaQ==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node-forge@1.3.14': + resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - '@types/node@22.18.1': - resolution: {integrity: sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==} + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/nodemailer@7.0.1': - resolution: {integrity: sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==} + '@types/node@24.10.8': + resolution: {integrity: sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ==} + + '@types/nodemailer@7.0.5': + resolution: {integrity: sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -5034,8 +5612,11 @@ packages: '@types/passport-strategy@0.2.38': resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} - '@types/passport@1.0.16': - resolution: {integrity: sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==} + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} '@types/prompts@2.4.9': resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} @@ -5043,30 +5624,60 @@ packages: '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} - '@types/qs@6.9.18': - resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@19.1.9': - resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 + + '@types/react-router-config@5.0.11': + resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} + + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} '@types/react-transition-group@4.4.12': resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} peerDependencies: '@types/react': '*' - '@types/react@19.1.12': - resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} + '@types/react@19.2.6': + resolution: {integrity: sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==} + + '@types/react@19.2.8': + resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} + + '@types/retry@0.12.2': + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + '@types/serve-index@1.9.4': + resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/sockjs@0.3.36': + resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -5080,75 +5691,84 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - '@types/uuid@9.0.8': - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@types/validator@13.12.0': - resolution: {integrity: sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==} + '@types/validator@13.15.4': + resolution: {integrity: sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/yargs@17.0.34': + resolution: {integrity: sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==} - '@typescript-eslint/eslint-plugin@8.43.0': - resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==} + '@typescript-eslint/eslint-plugin@8.46.3': + resolution: {integrity: sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.43.0 + '@typescript-eslint/parser': ^8.46.3 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.43.0': - resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} + '@typescript-eslint/parser@8.46.3': + resolution: {integrity: sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.43.0': - resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} + '@typescript-eslint/project-service@8.46.3': + resolution: {integrity: sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.43.0': - resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} + '@typescript-eslint/scope-manager@8.46.3': + resolution: {integrity: sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.43.0': - resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} + '@typescript-eslint/tsconfig-utils@8.46.3': + resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.43.0': - resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==} + '@typescript-eslint/type-utils@8.46.3': + resolution: {integrity: sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.43.0': - resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} + '@typescript-eslint/types@8.46.3': + resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.43.0': - resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} + '@typescript-eslint/typescript-estree@8.46.3': + resolution: {integrity: sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.43.0': - resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} + '@typescript-eslint/utils@8.46.3': + resolution: {integrity: sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.43.0': - resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} + '@typescript-eslint/visitor-keys@8.46.3': + resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -5300,40 +5920,40 @@ packages: '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 prom-client: ^15.0.0 - '@xhmikosr/archive-type@7.0.0': - resolution: {integrity: sha512-sIm84ZneCOJuiy3PpWR5bxkx3HaNt1pqaN+vncUBZIlPZCq8ASZH+hBVdu5H8znR7qYC6sKwx+ie2Q7qztJTxA==} - engines: {node: ^14.14.0 || >=16.0.0} + '@xhmikosr/archive-type@7.1.0': + resolution: {integrity: sha512-xZEpnGplg1sNPyEgFh0zbHxqlw5dtYg6viplmWSxUj12+QjU9SKu3U/2G73a15pEjLaOqTefNSZ1fOPUOT4Xgg==} + engines: {node: '>=18'} - '@xhmikosr/bin-check@7.0.3': - resolution: {integrity: sha512-4UnCLCs8DB+itHJVkqFp9Zjg+w/205/J2j2wNBsCEAm/BuBmtua2hhUOdAMQE47b1c7P9Xmddj0p+X1XVsfHsA==} + '@xhmikosr/bin-check@7.1.0': + resolution: {integrity: sha512-y1O95J4mnl+6MpVmKfMYXec17hMEwE/yeCglFNdx+QvLLtP0yN4rSYcbkXnth+lElBuKKek2NbvOfOGPpUXCvw==} engines: {node: '>=18'} - '@xhmikosr/bin-wrapper@13.0.5': - resolution: {integrity: sha512-DT2SAuHDeOw0G5bs7wZbQTbf4hd8pJ14tO0i4cWhRkIJfgRdKmMfkDilpaJ8uZyPA0NVRwasCNAmMJcWA67osw==} + '@xhmikosr/bin-wrapper@13.2.0': + resolution: {integrity: sha512-t9U9X0sDPRGDk5TGx4dv5xiOvniVJpXnfTuynVKwHgtib95NYEw4MkZdJqhoSiz820D9m0o6PCqOPMXz0N9fIw==} engines: {node: '>=18'} - '@xhmikosr/decompress-tar@8.0.1': - resolution: {integrity: sha512-dpEgs0cQKJ2xpIaGSO0hrzz3Kt8TQHYdizHsgDtLorWajuHJqxzot9Hbi0huRxJuAGG2qiHSQkwyvHHQtlE+fg==} + '@xhmikosr/decompress-tar@8.1.0': + resolution: {integrity: sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg==} engines: {node: '>=18'} - '@xhmikosr/decompress-tarbz2@8.0.1': - resolution: {integrity: sha512-OF+6DysDZP5YTDO8uHuGG6fMGZjc+HszFPBkVltjoje2Cf60hjBg/YP5OQndW1hfwVWOdP7f3CnJiPZHJUTtEg==} + '@xhmikosr/decompress-tarbz2@8.1.0': + resolution: {integrity: sha512-aCLfr3A/FWZnOu5eqnJfme1Z1aumai/WRw55pCvBP+hCGnTFrcpsuiaVN5zmWTR53a8umxncY2JuYsD42QQEbw==} engines: {node: '>=18'} - '@xhmikosr/decompress-targz@8.0.1': - resolution: {integrity: sha512-mvy5AIDIZjQ2IagMI/wvauEiSNHhu/g65qpdM4EVoYHUJBAmkQWqcPJa8Xzi1aKVTmOA5xLJeDk7dqSjlHq8Mg==} + '@xhmikosr/decompress-targz@8.1.0': + resolution: {integrity: sha512-fhClQ2wTmzxzdz2OhSQNo9ExefrAagw93qaG1YggoIz/QpI7atSRa7eOHv4JZkpHWs91XNn8Hry3CwUlBQhfPA==} engines: {node: '>=18'} - '@xhmikosr/decompress-unzip@7.0.0': - resolution: {integrity: sha512-GQMpzIpWTsNr6UZbISawsGI0hJ4KA/mz5nFq+cEoPs12UybAqZWKbyIaZZyLbJebKl5FkLpsGBkrplJdjvUoSQ==} + '@xhmikosr/decompress-unzip@7.1.0': + resolution: {integrity: sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA==} engines: {node: '>=18'} - '@xhmikosr/decompress@10.0.1': - resolution: {integrity: sha512-6uHnEEt5jv9ro0CDzqWlFgPycdE+H+kbJnwyxgZregIMLQ7unQSCNVsYG255FoqU8cP46DyggI7F7LohzEl8Ag==} + '@xhmikosr/decompress@10.2.0': + resolution: {integrity: sha512-MmDBvu0+GmADyQWHolcZuIWffgfnuTo4xpr2I/Qw5Ox0gt+e1Be7oYqJM4te5ylL6mzlcoicnHVDvP27zft8tg==} engines: {node: '>=18'} - '@xhmikosr/downloader@15.0.1': - resolution: {integrity: sha512-fiuFHf3Dt6pkX8HQrVBsK0uXtkgkVlhrZEh8b7VgoDqFf+zrgFBPyrwCqE/3nDwn3hLeNz+BsrS7q3mu13Lp1g==} + '@xhmikosr/downloader@15.2.0': + resolution: {integrity: sha512-lAqbig3uRGTt0sHNIM4vUG9HoM+mRl8K28WuYxyXLCUT6pyzl4Y4i0LZ3jMEsCYZ6zjPZbO9XkG91OSTd4si7g==} engines: {node: '>=18'} '@xhmikosr/os-filter-obj@3.0.0': @@ -5349,6 +5969,9 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@zone-eu/mailsplit@5.4.7': + resolution: {integrity: sha512-jApX86aDgolMz08pP20/J2zcns02NSK3zSiYouf01QQg4250L+GUAWSWicmS7eRvs+Z7wP7QfXrnkaTBGrIpwQ==} + abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -5356,6 +5979,10 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -5371,8 +5998,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.3: - resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} acorn@7.4.1: @@ -5380,25 +6007,23 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -5435,6 +6060,15 @@ packages: resolution: {integrity: sha512-XppPf2S42nO2WhvKzlwzlfcApcXHzjlod30pKmcWjRgLOtqoe5DMuqdiYoM6AgyXksc6A6pV4v1L/WW217e57w==} engines: {node: '>=0.8.0'} + algoliasearch-helper@3.26.1: + resolution: {integrity: sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + + algoliasearch@5.43.0: + resolution: {integrity: sha512-hbkK41JsuGYhk+atBDxlcKxskjDCh3OOEDpdKZPtw+3zucBqhlojRG5e5KtCmByGyYvwZswVeaSWglgLn2fibg==} + engines: {node: '>= 14.0.0'} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -5446,12 +6080,17 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} + ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: @@ -5462,16 +6101,12 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@3.17.0: - resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} - engines: {node: '>=14'} - - ansis@4.1.0: - resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} any-promise@1.3.0: @@ -5515,8 +6150,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-hidden@1.2.4: - resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} aria-query@5.3.0: @@ -5526,16 +6161,12 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - arktype@2.1.20: - resolution: {integrity: sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q==} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} array-includes@3.1.9: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} @@ -5544,6 +6175,10 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + array.prototype.findlast@1.2.5: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} @@ -5571,19 +6206,24 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - assert-never@1.3.0: - resolution: {integrity: sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==} + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + async-hook-jl@1.7.6: resolution: {integrity: sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==} engines: {node: ^4.7 || >=6.9 || >=7.3} - async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -5594,8 +6234,8 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -5615,8 +6255,8 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} - axe-core@4.10.3: - resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + axe-core@4.11.0: + resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} axios-auth-refresh@3.3.6: @@ -5624,28 +6264,43 @@ packages: peerDependencies: axios: '>= 0.18 < 0.19.0 || >= 0.19.1' - axios@1.11.0: - resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} - b4a@1.6.7: - resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true - babel-jest@30.1.2: - resolution: {integrity: sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==} + babel-jest@30.2.0: + resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': ^7.11.0 + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-loader@9.2.1: + resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' - babel-plugin-istanbul@7.0.0: - resolution: {integrity: sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==} + babel-plugin-dynamic-import-node@2.3.3: + resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} engines: {node: '>=12'} - babel-plugin-jest-hoist@30.0.1: - resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==} + babel-plugin-jest-hoist@30.2.0: + resolution: {integrity: sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} babel-plugin-macros@3.1.0: @@ -5672,25 +6327,40 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 - babel-preset-jest@30.0.1: - resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==} + babel-preset-jest@30.2.0: + resolution: {integrity: sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': ^7.11.0 + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 babel-walk@3.0.0-canary-5: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.5.0: - resolution: {integrity: sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==} + bare-events@2.8.1: + resolution: {integrity: sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.7: + resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} + hasBin: true + + batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + bcrypt@6.0.0: resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} engines: {node: '>= 18'} @@ -5699,6 +6369,9 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + bin-version-check@5.1.0: resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==} engines: {node: '>=12'} @@ -5723,37 +6396,52 @@ packages: bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} + bonjour-service@1.3.0: + resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.11.0: - resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} boxen@5.1.2: resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} engines: {node: '>=10'} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + boxen@6.2.1: + resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -5787,6 +6475,10 @@ packages: resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} engines: {node: '>=0.2.0'} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5797,6 +6489,10 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -5821,10 +6517,6 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -5836,6 +6528,9 @@ packages: camel-case@3.0.0: resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -5848,11 +6543,18 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001707: - resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001731: - resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} + caniuse-lite@1.0.30001760: + resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} chainsaw@0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} @@ -5865,6 +6567,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -5872,11 +6578,23 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} check-disk-space@3.4.0: resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} @@ -5911,8 +6629,8 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - ci-info@4.3.0: - resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} cjs-module-lexer@2.1.0: @@ -5921,8 +6639,8 @@ packages: class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} - class-validator@0.14.2: - resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==} + class-validator@0.14.3: + resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==} class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -5934,10 +6652,22 @@ packages: resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} engines: {node: '>= 4.0'} + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-boxes@2.2.1: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -5961,6 +6691,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -5983,8 +6717,11 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -5993,12 +6730,8 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} @@ -6006,16 +6739,23 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combine-promises@1.2.0: + resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} + engines: {node: '>=10'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - commander@14.0.0: - resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} commander@2.20.3: @@ -6025,6 +6765,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -6037,10 +6781,13 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-json@4.2.5: - resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + comment-json@4.4.1: + resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} engines: {node: '>= 6'} + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -6051,6 +6798,14 @@ packages: resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} engines: {node: '>= 10'} + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -6064,6 +6819,14 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + configstore@6.0.0: + resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} + engines: {node: '>=12'} + + connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -6071,13 +6834,17 @@ packages: constantinople@4.0.1: resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + content-disposition@0.5.2: + resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} + engines: {node: '>= 0.6'} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -6089,10 +6856,17 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -6104,8 +6878,8 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - cookies-next@6.1.0: - resolution: {integrity: sha512-8MqWliHg6YRatqlup5HlKCqXM5cFtwq9BVowDpPniPfbTOmrfIEXUQOcRFVXQltV+hyvKDRGJPNtceICkiJ/IA==} + cookies-next@6.1.1: + resolution: {integrity: sha512-8JZBc4IN2m8xqOXyG7uicth6yH3SvynAUGSeV/FQ5lcEclzNGoR0+YCjySdn8r6keZdRyGQr4YJ8qy8F+Jq5uw==} peerDependencies: next: '>=15.0.0' react: '>= 16.8.0' @@ -6113,14 +6887,20 @@ packages: copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} - core-js-compat@3.45.0: - resolution: {integrity: sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==} + copy-webpack-plugin@11.0.0: + resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.1.0 + + core-js-compat@3.46.0: + resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} - core-js-pure@3.39.0: - resolution: {integrity: sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==} + core-js-pure@3.46.0: + resolution: {integrity: sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==} - core-js@3.39.0: - resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + core-js@3.46.0: + resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -6158,8 +6938,8 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cron@4.3.0: - resolution: {integrity: sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==} + cron@4.3.3: + resolution: {integrity: sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==} engines: {node: '>=18.x'} cross-spawn@6.0.6: @@ -6170,13 +6950,81 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-in-js-utils@3.1.0: - resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} - css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-blank-pseudo@7.0.1: + resolution: {integrity: sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 - css-tree@1.1.3: + css-declaration-sorter@7.3.0: + resolution: {integrity: sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-has-pseudo@7.0.3: + resolution: {integrity: sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css-minimizer-webpack-plugin@5.0.1: + resolution: {integrity: sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@parcel/css': '*' + '@swc/css': '*' + clean-css: '*' + csso: '*' + esbuild: '*' + lightningcss: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@parcel/css': + optional: true + '@swc/css': + optional: true + clean-css: + optional: true + csso: + optional: true + esbuild: + optional: true + lightningcss: + optional: true + + css-prefers-color-scheme@10.0.0: + resolution: {integrity: sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} @@ -6188,18 +7036,45 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssdb@8.4.2: + resolution: {integrity: sha512-PzjkRkRUS+IHDJohtxkIczlxPPZqRo0nXplsYXOMBRPjcVRjj1W4DfvRgshUYTVuUigU7ptVYkFJQ7abUB0nyg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssnano-preset-advanced@6.1.2: + resolution: {integrity: sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-preset-default@6.1.2: + resolution: {integrity: sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-utils@4.0.2: + resolution: {integrity: sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano@6.1.2: + resolution: {integrity: sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -6208,8 +7083,8 @@ packages: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} @@ -6280,14 +7155,19 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - dayjs@1.11.11: - resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} - dayjs@1.11.18: - resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -6297,8 +7177,8 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -6312,12 +7192,15 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} - dedent@1.6.0: - resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + dedent@1.7.0: + resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -6335,12 +7218,20 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - defaults@3.0.0: - resolution: {integrity: sha512-RsqXDEAALjfRTro+IFNKpcPCt0/Cy2FqHSIlnomiJp9YGadpQnrtbRpSgN2+np21qHcIKiva4fiOQGjS9/qR/A==} - engines: {node: '>=18'} + defaults@2.0.2: + resolution: {integrity: sha512-cuIw0PImdp76AOfgkjbW4VhQODRmNNcKR73vdCH5cLd/ifj7aamfoXvYgfGkEAjNJZ3ozMIy9Gu2LutUkGEPbA==} + engines: {node: '>=16'} defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} @@ -6350,6 +7241,14 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -6362,6 +7261,10 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -6370,12 +7273,16 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} detect-newline@3.1.0: @@ -6388,6 +7295,14 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + detect-port@1.6.1: + resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} + engines: {node: '>= 4.0.0'} + hasBin: true + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -6398,6 +7313,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + display-notification@2.0.0: resolution: {integrity: sha512-TdmtlAcdqy1NU+j7zlkDdMnCL878zriLaBmoD9quOoq1ySSSGv03l0hXK5CvIFZlIfFI/hizqdQuW+Num7xuhw==} engines: {node: '>=4'} @@ -6405,6 +7324,10 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -6418,6 +7341,9 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -6445,12 +7371,16 @@ packages: domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dotenv-expand@12.0.1: resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} engines: {node: '>=12'} @@ -6463,12 +7393,12 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dotenv@17.2.2: - resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -6478,6 +7408,9 @@ packages: duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6497,11 +7430,11 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.114: - resolution: {integrity: sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==} + electron-to-chromium@1.5.245: + resolution: {integrity: sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==} - electron-to-chromium@1.5.195: - resolution: {integrity: sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} emitter-listener@1.1.2: resolution: {integrity: sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==} @@ -6516,23 +7449,33 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + emoticon@4.1.0: + resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - encoding-japanese@2.0.0: - resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==} - engines: {node: '>=8.10.0'} - - encoding-japanese@2.1.0: - resolution: {integrity: sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==} + encoding-japanese@2.2.0: + resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} engines: {node: '>=8.10.0'} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} entities@2.2.0: @@ -6546,16 +7489,12 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} - engines: {node: '>= 0.4'} - es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -6575,6 +7514,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -6583,9 +7525,6 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} - es-shim-unscopables@1.1.0: resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} engines: {node: '>= 0.4'} @@ -6594,8 +7533,17 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + es-toolkit@1.41.0: + resolution: {integrity: sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} engines: {node: '>=18'} hasBin: true @@ -6607,6 +7555,10 @@ packages: resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} engines: {node: '>=10'} + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -6614,6 +7566,10 @@ packages: resolution: {integrity: sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA==} engines: {node: '>=0.10.0'} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -6622,6 +7578,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -6668,20 +7628,26 @@ packages: peerDependencies: eslint: '>=7' - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} + eslint-plugin-react-hooks@6.1.1: + resolution: {integrity: sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==} + 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 + eslint-plugin-react-perf@3.3.3: + resolution: {integrity: sha512-EzPdxsRJg5IllCAH9ny/3nK7sv9251tvKmi/d3Ouv5KzI8TB3zNhzScxL9wnh9Hvv8GYC5LEtzTauynfOEYiAw==} + engines: {node: '>=6.9.1'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-turbo@2.5.6: - resolution: {integrity: sha512-KUDE23aP2JV8zbfZ4TeM1HpAXzMM/AYG/bJam7P4AalUxas8Pd/lS/6R3p4uX91qJcH1LwL4h0ED48nDe8KorQ==} + eslint-plugin-turbo@2.7.5: + resolution: {integrity: sha512-dHUEEZyoUriaYqaqu6nlnkj0W+iDPYFhuMjLYGxn7p767kLCYTZgjR42urW56VLiRQUqDL+foOXufFM6+kXCEQ==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' @@ -6702,8 +7668,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.35.0: - resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -6749,23 +7715,61 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@3.5.0: + resolution: {integrity: sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eta@2.2.0: + resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} + engines: {node: '>=6.0.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eval@0.1.8: + resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} + engines: {node: '>= 0.8'} + eventemitter2@6.4.9: resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6786,16 +7790,16 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} - expect@30.0.5: - resolution: {integrity: sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==} + expect@30.2.0: + resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - expect@30.1.2: - resolution: {integrity: sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} ext-list@2.2.2: @@ -6809,12 +7813,15 @@ packages: extend-object@1.0.0: resolution: {integrity: sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw==} - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} - fast-copy@3.0.2: - resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-copy@4.0.0: + resolution: {integrity: sha512-/oA0gx1xyXE9R2YlV4FXwZJXngFdm9Du0zN8FhY38jnLkhp1u35h6bCyKgRhlsA6C9I+1vfXE4KISdt7xc6M9w==} fast-csv@4.3.6: resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} @@ -6830,10 +7837,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.2.2: - resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} - engines: {node: '>=6.0.0'} - fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -6848,8 +7851,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-json-stringify@6.0.1: - resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==} + fast-json-stringify@6.1.1: + resolution: {integrity: sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -6857,18 +7860,14 @@ packages: fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - fast-redact@3.5.0: - resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} - engines: {node: '>=6'} - fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} fast-xml-parser@5.2.5: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} @@ -6877,47 +7876,64 @@ packages: fastest-stable-stringify@2.0.2: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} - fastify-plugin@5.0.1: - resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify@5.6.2: + resolution: {integrity: sha512-dPugdGnsvYkBlENLhCgX8yhyGCsCPrpA8lFWbTNU428l+YOnLgYHR69hzV8HWPC79n536EqzqQtvhtdaCE0dKg==} - fastify@5.4.0: - resolution: {integrity: sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==} + fastify@5.7.1: + resolution: {integrity: sha512-ZW7S4fxlZhE+tYWVokFzjh+i56R+buYKNGhrVl6DtN8sxkyMEzpJnzvO8A/ZZrsg5w6X37u6I4EOQikYS5DXpA==} fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true + feed@4.2.2: + resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} + engines: {node: '>=0.4.0'} fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-type@19.6.0: - resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} + file-loader@6.2.0: + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + file-type@20.5.0: + resolution: {integrity: sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==} engines: {node: '>=18'} - file-type@21.0.0: - resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} + file-type@21.3.0: + resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} filelist@1.0.4: @@ -6935,12 +7951,20 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} - find-my-way@9.3.0: - resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} + find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} + + find-my-way@9.4.0: + resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} engines: {node: '>=20'} find-root@1.1.0: @@ -6954,6 +7978,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + find-versions@5.1.0: resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==} engines: {node: '>=12'} @@ -6969,11 +7997,15 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -6981,9 +8013,6 @@ packages: debug: optional: true - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -7003,14 +8032,18 @@ packages: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} - engines: {node: '>= 6'} - form-data@4.0.4: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + formidable@3.5.4: resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} engines: {node: '>=14.0.0'} @@ -7019,11 +8052,11 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.23.12: - resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} + framer-motion@12.27.1: + resolution: {integrity: sha512-cEAqO69kcZt3gL0TGua8WTgRQfv4J57nqt1zxHtLKwYhAwA0x9kDS/JbMa1hJbwkGY74AGJKvZ9pX/IqWZtZWQ==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -7036,6 +8069,10 @@ packages: react-dom: optional: true + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -7047,8 +8084,12 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-monkey@1.0.6: - resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -7081,6 +8122,10 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -7097,6 +8142,9 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -7117,14 +8165,13 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + github-slugger@1.5.0: + resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -7133,6 +8180,12 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regex.js@1.2.0: + resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -7141,19 +8194,22 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true - glob@11.0.3: - resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} - hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -7162,20 +8218,46 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + got@13.0.0: resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==} engines: {node: '>=16'} + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -7189,10 +8271,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-own-prop@2.0.0: - resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} - engines: {node: '>=8'} - has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -7208,10 +8286,38 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + has-yarn@3.0.0: + resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -7225,9 +8331,15 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + hpagent@1.2.0: resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} engines: {node: '>=14'} @@ -7239,6 +8351,16 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + html-minifier@4.0.0: resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} engines: {node: '>=6'} @@ -7247,30 +8369,75 @@ packages: html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + html-to-text@9.0.5: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + html-webpack-plugin@5.6.4: + resolution: {integrity: sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + htmlparser2@5.0.1: resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-proxy-middleware@2.0.9: + resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + http2-wrapper@2.2.1: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} @@ -7283,14 +8450,18 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} i18next-fs-backend@2.6.0: resolution: {integrity: sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==} - i18next@25.5.2: - resolution: {integrity: sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==} + i18next@25.7.4: + resolution: {integrity: sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -7309,6 +8480,16 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -7316,20 +8497,32 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.4: - resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - immer@10.1.3: - resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.3: + resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -7343,20 +8536,34 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - index-to-position@1.1.0: - resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} + infima@0.2.0-alpha.45: + resolution: {integrity: sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==} + engines: {node: '>=12'} + inflight@1.0.6: resolution: {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. + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + inline-style-parser@0.2.6: + resolution: {integrity: sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==} + inline-style-prefixer@7.0.1: resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} @@ -7371,6 +8578,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -7379,6 +8589,12 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -7386,11 +8602,8 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - - is-async-function@2.1.0: - resolution: {integrity: sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} is-bigint@1.1.0: @@ -7401,14 +8614,18 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-boolean-object@1.2.1: - resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -7421,14 +8638,26 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-expression@4.0.0: resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -7445,14 +8674,26 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -7465,6 +8706,14 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-network-error@1.3.0: + resolution: {integrity: sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==} + engines: {node: '>=16'} + + is-npm@6.1.0: + resolution: {integrity: sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -7473,10 +8722,34 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -7496,6 +8769,10 @@ packages: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -7512,10 +8789,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -7528,6 +8801,9 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -7536,10 +8812,6 @@ packages: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} - is-weakref@1.1.0: - resolution: {integrity: sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==} - engines: {node: '>= 0.4'} - is-weakref@1.1.1: resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} engines: {node: '>= 0.4'} @@ -7552,6 +8824,17 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + is-yarn-global@0.4.1: + resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} + engines: {node: '>=12'} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -7561,6 +8844,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -7577,8 +8864,8 @@ packages: resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} iterare@1.2.1: @@ -7596,25 +8883,21 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.1.1: - resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} - engines: {node: 20 || >=22} - - jake@10.9.2: - resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} engines: {node: '>=10'} hasBin: true - jest-changed-files@30.0.5: - resolution: {integrity: sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==} + jest-changed-files@30.2.0: + resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-circus@30.1.3: - resolution: {integrity: sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==} + jest-circus@30.2.0: + resolution: {integrity: sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-cli@30.1.3: - resolution: {integrity: sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==} + jest-cli@30.2.0: + resolution: {integrity: sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -7623,8 +8906,8 @@ packages: node-notifier: optional: true - jest-config@30.1.3: - resolution: {integrity: sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==} + jest-config@30.2.0: + resolution: {integrity: sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@types/node': '*' @@ -7638,24 +8921,20 @@ packages: ts-node: optional: true - jest-diff@30.0.5: - resolution: {integrity: sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==} + jest-diff@30.2.0: + resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-diff@30.1.2: - resolution: {integrity: sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==} + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-docblock@30.0.1: - resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==} + jest-each@30.2.0: + resolution: {integrity: sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-each@30.1.0: - resolution: {integrity: sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-environment-jsdom@30.1.2: - resolution: {integrity: sha512-LXsfAh5+mDTuXDONGl1ZLYxtJEaS06GOoxJb2arcJTjIfh1adYg8zLD8f6P0df8VmjvCaMrLmc1PgHUI/YUTbg==} + jest-environment-jsdom@30.2.0: + resolution: {integrity: sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -7663,42 +8942,34 @@ packages: canvas: optional: true - jest-environment-node@30.1.2: - resolution: {integrity: sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==} + jest-environment-node@30.2.0: + resolution: {integrity: sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-fixed-jsdom@0.0.10: - resolution: {integrity: sha512-WaEVX+FripJh+Hn/7dysIgqP66h0KT1NNC22NGmNYANExtCoYNk1q2yjwwcdSboBMkkhn0NtmvKad/cmisnCLg==} + jest-fixed-jsdom@0.0.11: + resolution: {integrity: sha512-3UkjgM79APnmLVDnelrxdwz4oybD5qw6NLyayl7iCX8C8tJHeqjL9fmNrRlIrNiVJSXkF5t9ZPJ+xlM0kSwwYg==} engines: {node: '>=18.0.0'} peerDependencies: jest-environment-jsdom: '>=28.0.0' - jest-haste-map@30.1.0: - resolution: {integrity: sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-leak-detector@30.1.0: - resolution: {integrity: sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==} + jest-haste-map@30.2.0: + resolution: {integrity: sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-matcher-utils@30.0.5: - resolution: {integrity: sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==} + jest-leak-detector@30.2.0: + resolution: {integrity: sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-matcher-utils@30.1.2: - resolution: {integrity: sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==} + jest-matcher-utils@30.2.0: + resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-message-util@30.0.5: - resolution: {integrity: sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==} + jest-message-util@30.2.0: + resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-message-util@30.1.0: - resolution: {integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - jest-mock@30.0.5: - resolution: {integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==} + jest-mock@30.2.0: + resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-pnp-resolver@1.2.3: @@ -7714,48 +8985,56 @@ packages: resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve-dependencies@30.1.3: - resolution: {integrity: sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==} + jest-resolve-dependencies@30.2.0: + resolution: {integrity: sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve@30.1.3: - resolution: {integrity: sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==} + jest-resolve@30.2.0: + resolution: {integrity: sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runner@30.1.3: - resolution: {integrity: sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==} + jest-runner@30.2.0: + resolution: {integrity: sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-runtime@30.1.3: - resolution: {integrity: sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==} + jest-runtime@30.2.0: + resolution: {integrity: sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-snapshot@30.1.2: - resolution: {integrity: sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==} + jest-snapshot@30.2.0: + resolution: {integrity: sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-util@30.0.5: - resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==} + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@30.2.0: + resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-validate@30.1.0: - resolution: {integrity: sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==} + jest-validate@30.2.0: + resolution: {integrity: sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-watcher@30.1.3: - resolution: {integrity: sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==} + jest-watcher@30.2.0: + resolution: {integrity: sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jest-worker@30.1.0: - resolution: {integrity: sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==} + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@30.2.0: + resolution: {integrity: sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest@30.1.3: - resolution: {integrity: sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==} + jest@30.2.0: + resolution: {integrity: sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: @@ -7768,20 +9047,19 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} - hasBin: true + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - joi@18.0.1: - resolution: {integrity: sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==} + joi@18.0.2: + resolution: {integrity: sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==} engines: {node: '>= 20'} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-beautify@1.15.1: - resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} engines: {node: '>=14'} hasBin: true @@ -7809,8 +9087,8 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsdom@26.1.0: @@ -7822,11 +9100,6 @@ packages: canvas: optional: true - jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -7838,8 +9111,8 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-ref-resolver@2.0.1: - resolution: {integrity: sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7866,13 +9139,17 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} @@ -7883,17 +9160,23 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - juice@10.0.0: - resolution: {integrity: sha512-9f68xmhGrnIi6DBkiiP3rUrQN33SEuaKu1+njX6VgMP+jwZAsnT33WIzlrWICL9matkhYu3OyrqSUP55YTIdGg==} + juice@10.0.1: + resolution: {integrity: sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==} engines: {node: '>=10.0.0'} hasBin: true - jwa@1.4.1: - resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + jwt-decode@4.0.0: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} @@ -7916,6 +9199,13 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + + launch-editor@2.12.0: + resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -7931,26 +9221,17 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libbase64@1.2.1: - resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==} - libbase64@1.3.0: resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} - libmime@5.2.0: - resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==} + libmime@5.3.7: + resolution: {integrity: sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==} - libmime@5.3.5: - resolution: {integrity: sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==} + libphonenumber-js@1.12.25: + resolution: {integrity: sha512-u90tUu/SEF8b+RaDKCoW7ZNFDakyBtFlX1ex3J+VH+ElWes/UaitJLt/w4jGu8uAE41lltV/s+kMVtywcMEg7g==} - libphonenumber-js@1.11.4: - resolution: {integrity: sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q==} - - libqp@2.0.1: - resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==} - - libqp@2.1.0: - resolution: {integrity: sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==} + libqp@2.1.1: + resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -7977,29 +9258,33 @@ packages: linkify@0.2.1: resolution: {integrity: sha512-Esq4rfRRHmH3RTqAsnGq33MwE0HqyeNWHpNbWFsV8jEnuPKseiOPjzWvK2i3REqwZpnbCxTh1yxAKzceKKFDGQ==} - linkifyjs@4.2.0: - resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==} + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} - liquidjs@10.15.0: - resolution: {integrity: sha512-u5lYWhW8ioT+O3FdCcp5U+hiPEGNO4xASCFlCHA+k5rMTJwDIa2c2KF111ZDKc2xGM7LXPvMoNRIrBfbLNpRBg==} - engines: {node: '>=14'} + liquidjs@10.24.0: + resolution: {integrity: sha512-TAUNAdgwaAXjjcUFuYVJm9kOVH7zc0mTKxsG9t9Lu4qdWjB2BEblyVIYpjWcmJLMGgiYqnGNJjpNMHx0gp/46A==} + engines: {node: '>=16'} hasBin: true listenercount@1.0.1: resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} - load-esm@1.0.2: - resolution: {integrity: sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==} + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} engines: {node: '>=13.2.0'} load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -8008,6 +9293,10 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} @@ -8069,9 +9358,6 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.union@4.6.0: resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} @@ -8085,8 +9371,11 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - long@5.3.1: - resolution: {integrity: sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -8105,30 +9394,22 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.0.2: - resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} engines: {node: 20 || >=22} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - lru.min@1.1.2: resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@0.543.0: - resolution: {integrity: sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA==} + lucide-react@0.562.0: + resolution: {integrity: sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - luxon@3.6.1: - resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} - engines: {node: '>=12'} - luxon@3.7.2: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} @@ -8143,11 +9424,11 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - mailparser@3.7.1: - resolution: {integrity: sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - mailsplit@5.4.0: - resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==} + mailparser@3.9.0: + resolution: {integrity: sha512-jpaNLhDjwy0w2f8sySOSRiWREjPqssSc0C2czV98btCXCRX3EyNloQ2IWirmMDj1Ies8Fkm0l96bZBZpDG7qkg==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -8159,10 +9440,74 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} @@ -8184,12 +9529,18 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memfs@4.51.0: + resolution: {integrity: sha512-4zngfkVM/GpIhC8YazOsM6E8hoB33NP0BCESPOA6z7qaL6umPJNqkO8CNYaLV2FB2MV6H1O3x2luHHOSqppv+A==} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} mensch@0.3.4: resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -8205,10 +9556,137 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + engines: {node: '>= 0.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -8217,6 +9695,10 @@ packages: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.18: + resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -8225,6 +9707,11 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -8251,8 +9738,17 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + mini-css-extract-plugin@2.9.4: + resolution: {integrity: sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} minimatch@3.1.2: @@ -8280,120 +9776,127 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - mjml-accordion@4.15.3: - resolution: {integrity: sha512-LPNVSj1LyUVYT9G1gWwSw3GSuDzDsQCu0tPB2uDsq4VesYNnU6v3iLCQidMiR6azmIt13OEozG700ygAUuA6Ng==} + mjml-accordion@4.16.1: + resolution: {integrity: sha512-WqBaDmov7uI15dDVZ5UK6ngNwVhhXawW+xlCVbjs21wmskoG4lXc1j+28trODqGELk3BcQOqjO8Ee6Ytijp4PA==} - mjml-body@4.15.3: - resolution: {integrity: sha512-7pfUOVPtmb0wC+oUOn4xBsAw4eT5DyD6xqaxj/kssu6RrFXOXgJaVnDPAI9AzIvXJ/5as9QrqRGYAddehwWpHQ==} + mjml-body@4.16.1: + resolution: {integrity: sha512-A19pJ2HXqc7A5pKc8Il/d1cH5yyO2Jltwit3eUKDrZ/fBfYxVWZVPNuMooqt6QyC26i+xhhVbVsRNTwL1Aclqg==} - mjml-button@4.15.3: - resolution: {integrity: sha512-79qwn9AgdGjJR1vLnrcm2rq2AsAZkKC5JPwffTMG+Nja6zGYpTDZFZ56ekHWr/r1b5WxkukcPj2PdevUug8c+Q==} + mjml-button@4.16.1: + resolution: {integrity: sha512-z2YsSEDHU4ubPMLAJhgopq3lnftjRXURmG8A+K/QIH4Js6xHIuSNzCgVbBl13/rB1hwc2RxUP839JoLt3M1FRg==} - mjml-carousel@4.15.3: - resolution: {integrity: sha512-3ju6I4l7uUhPRrJfN3yK9AMsfHvrYbRkcJ1GRphFHzUj37B2J6qJOQUpzA547Y4aeh69TSb7HFVf1t12ejQxVw==} + mjml-carousel@4.16.1: + resolution: {integrity: sha512-Xna+lSHJGMiPxDG3kvcK3OfEDQbkgyXEz0XebN7zpLDs1Mo4IXe8qI7fFnDASckwC14gmdPwh/YcLlQ4nkzwrQ==} - mjml-cli@4.15.3: - resolution: {integrity: sha512-+V2TDw3tXUVEptFvLSerz125C2ogYl8klIBRY1m5BHd4JvGVf3yhx8N3PngByCzA6PGcv/eydGQN+wy34SHf0Q==} + mjml-cli@4.16.1: + resolution: {integrity: sha512-1dTGWOKucdNImjLzDZfz1+aWjjZW4nRW5pNUMOdcIhgGpygYGj1X4/R8uhrC61CGQXusUrHyojQNVks/aBm9hQ==} hasBin: true - mjml-column@4.15.3: - resolution: {integrity: sha512-hYdEFdJGHPbZJSEysykrevEbB07yhJGSwfDZEYDSbhQQFjV2tXrEgYcFD5EneMaowjb55e3divSJxU4c5q4Qgw==} + mjml-column@4.16.1: + resolution: {integrity: sha512-olScfxGEC0hp3VGzJUn7/znu7g9QlU1PsVRNL7yGKIUiZM/foysYimErBq2CfkF+VkEA9ZlMMeRLGNFEW7H3qQ==} - mjml-core@4.15.3: - resolution: {integrity: sha512-Dmwk+2cgSD9L9GmTbEUNd8QxkTZtW9P7FN/ROZW/fGZD6Hq6/4TB0zEspg2Ow9eYjZXO2ofOJ3PaQEEShKV0kQ==} + mjml-core@4.16.1: + resolution: {integrity: sha512-sT7VbcUyd3m68tyZvK/cYbZIn7J3E4A+AFtAxI2bxj4Mz8QPjpz6BUGXkRJcYYxvNYVA+2rBFCFRXe5ErsVMVg==} - mjml-divider@4.15.3: - resolution: {integrity: sha512-vh27LQ9FG/01y0b9ntfqm+GT5AjJnDSDY9hilss2ixIUh0FemvfGRfsGVeV5UBVPBKK7Ffhvfqc7Rciob9Spzw==} + mjml-divider@4.16.1: + resolution: {integrity: sha512-KNqk0V3VRXU0f3yoziFUl1TboeRJakm+7B7NmGRUj13AJrEkUela2Y4/u0wPk8GMC8Qd25JTEdbVHlImfyNIQQ==} - mjml-group@4.15.3: - resolution: {integrity: sha512-HSu/rKnGZVKFq3ciT46vi1EOy+9mkB0HewO4+P6dP/Y0UerWkN6S3UK11Cxsj0cAp0vFwkPDCdOeEzRdpFEkzA==} + mjml-group@4.16.1: + resolution: {integrity: sha512-pjNEpS9iTh0LGeYZXhfhI27pwFFTAiqx+5Q420P4ebLbeT5Vsmr8TrcaB/gEPNn/eLrhzH/IssvnFOh5Zlmrlg==} - mjml-head-attributes@4.15.3: - resolution: {integrity: sha512-2ISo0r5ZKwkrvJgDou9xVPxxtXMaETe2AsAA02L89LnbB2KC0N5myNsHV0sEysTw9+CfCmgjAb0GAI5QGpxKkQ==} + mjml-head-attributes@4.16.1: + resolution: {integrity: sha512-JHFpSlQLJomQwKrdptXTdAfpo3u3bSezM/4JfkCi53MBmxNozWzQ/b8lX3fnsTSf9oywkEEGZD44M2emnTWHug==} - mjml-head-breakpoint@4.15.3: - resolution: {integrity: sha512-Eo56FA5C2v6ucmWQL/JBJ2z641pLOom4k0wP6CMZI2utfyiJ+e2Uuinj1KTrgDcEvW4EtU9HrfAqLK9UosLZlg==} + mjml-head-breakpoint@4.16.1: + resolution: {integrity: sha512-b4C/bZCMV1k/br2Dmqfp/mhYPkcZpBQdMpAOAaI8na7HmdS4rE/seJUfeCUr7fy/7BvbmsN2iAAttP54C4bn/A==} - mjml-head-font@4.15.3: - resolution: {integrity: sha512-CzV2aDPpiNIIgGPHNcBhgyedKY4SX3BJoTwOobSwZVIlEA6TAWB4Z9WwFUmQqZOgo1AkkiTHPZQvGcEhFFXH6g==} + mjml-head-font@4.16.1: + resolution: {integrity: sha512-Bw3s5HSeWX3wVq4EJnBS8OOgw/RP4zO0pbidv7T+VqKunUEuUwCEaLZyuTyhBqJ61QiPOehBBGBDGwYyVaJGVg==} - mjml-head-html-attributes@4.15.3: - resolution: {integrity: sha512-MDNDPMBOgXUZYdxhosyrA2kudiGO8aogT0/cODyi2Ed9o/1S7W+je11JUYskQbncqhWKGxNyaP4VWa+6+vUC/g==} + mjml-head-html-attributes@4.16.1: + resolution: {integrity: sha512-GtT0vb6rb/dyrdPzlMQTtMjCwUyXINAHcUR+IGi1NTx8xoHWUjmWPQ/v95IhgelsuQgynuLWVPundfsPn8/PTQ==} - mjml-head-preview@4.15.3: - resolution: {integrity: sha512-J2PxCefUVeFwsAExhrKo4lwxDevc5aKj888HBl/wN4EuWOoOg06iOGCxz4Omd8dqyFsrqvbBuPqRzQ+VycGmaA==} + mjml-head-preview@4.16.1: + resolution: {integrity: sha512-5iDM5ZO0JWgucIFJG202kGKVQQWpn1bOrySIIp2fQn1hCXQaefAPYduxu7xDRtnHeSAw623IxxKzZutOB8PMSg==} - mjml-head-style@4.15.3: - resolution: {integrity: sha512-9J+JuH+mKrQU65CaJ4KZegACUgNIlYmWQYx3VOBR/tyz+8kDYX7xBhKJCjQ1I4wj2Tvga3bykd89Oc2kFZ5WOw==} + mjml-head-style@4.16.1: + resolution: {integrity: sha512-P6NnbG3+y1Ow457jTifI9FIrpkVSxEHTkcnDXRtq3fA5UR7BZf3dkrWQvsXelm6DYCSGUY0eVuynPPOj71zetQ==} - mjml-head-title@4.15.3: - resolution: {integrity: sha512-IM59xRtsxID4DubQ0iLmoCGXguEe+9BFG4z6y2xQDrscIa4QY3KlfqgKGT69ojW+AVbXXJPEVqrAi4/eCsLItQ==} + mjml-head-title@4.16.1: + resolution: {integrity: sha512-s7X9XkIu46xKXvjlZBGkpfsTcgVqpiQjAm0OrHRV9E5TLaICoojmNqEz5CTvvlTz7olGoskI1gzJlnhKxPmkXQ==} - mjml-head@4.15.3: - resolution: {integrity: sha512-o3mRuuP/MB5fZycjD3KH/uXsnaPl7Oo8GtdbJTKtH1+O/3pz8GzGMkscTKa97l03DAG2EhGrzzLcU2A6eshwFw==} + mjml-head@4.16.1: + resolution: {integrity: sha512-R/YA6wxnUZHknJ2H7TT6G6aXgNY7B3bZrAbJQ4I1rV/l0zXL9kfjz2EpkPfT0KHzS1cS2J1pK/5cn9/KHvHA2Q==} - mjml-hero@4.15.3: - resolution: {integrity: sha512-9cLAPuc69yiuzNrMZIN58j+HMK1UWPaq2i3/Fg2ZpimfcGFKRcPGCbEVh0v+Pb6/J0+kf8yIO0leH20opu3AyQ==} + mjml-hero@4.16.1: + resolution: {integrity: sha512-1q6hsG7l2hgdJeNjSNXVPkvvSvX5eJR5cBvIkSbIWqT297B1WIxwcT65Nvfr1FpkEALeswT4GZPSfvTuXyN8hg==} - mjml-image@4.15.3: - resolution: {integrity: sha512-g1OhSdofIytE9qaOGdTPmRIp7JsCtgO0zbsn1Fk6wQh2gEL55Z40j/VoghslWAWTgT2OHFdBKnMvWtN6U5+d2Q==} + mjml-image@4.16.1: + resolution: {integrity: sha512-snTULRoskjMNPxajSFIp4qA/EjZ56N0VXsAfDQ9ZTXZs0Mo3vy2N81JDGNVRmKkAJyPEwN77zrAHbic0Ludm1w==} - mjml-migrate@4.15.3: - resolution: {integrity: sha512-sr/+35RdxZroNQVegjpfRHJ5hda9XCgaS4mK2FGO+Mb1IUevKfeEPII3F/cHDpNwFeYH3kAgyqQ22ClhGLWNBA==} + mjml-migrate@4.16.1: + resolution: {integrity: sha512-4SuaFWyu1Hg948ODHz1gF5oXrhgRI1LgtWMRE+Aoz4F6SSA7kL78iJqEVvouOHCpcxQStDdiZo8/KeuQ1llEAw==} hasBin: true - mjml-navbar@4.15.3: - resolution: {integrity: sha512-VsKH/Jdlf8Yu3y7GpzQV5n7JMdpqvZvTSpF6UQXL0PWOm7k6+LX+sCZimOfpHJ+wCaaybpxokjWZ71mxOoCWoA==} + mjml-navbar@4.16.1: + resolution: {integrity: sha512-lLlTOU3pVvlnmIJ/oHbyuyV8YZ99mnpRvX+1ieIInFElOchEBLoq1Mj+RRfaf2EV/q3MCHPyYUZbDITKtqdMVg==} - mjml-parser-xml@4.15.3: - resolution: {integrity: sha512-Tz0UX8/JVYICLjT+U8J1f/TFxIYVYjzZHeh4/Oyta0pLpRLeZlxEd71f3u3kdnulCKMP4i37pFRDmyLXAlEuLw==} + mjml-parser-xml@4.16.1: + resolution: {integrity: sha512-QsHnPgVGgzcLX82wn1uP53X9pIUP3H6bJMad9R1v2F1A9rhaKK+wctxvXWBp4+XXJOv3SqpE5GDBEQPWNs5IgQ==} - mjml-preset-core@4.15.3: - resolution: {integrity: sha512-1zZS8P4O0KweWUqNS655+oNnVMPQ1Rq1GaZq5S9JfwT1Vh/m516lSmiTW9oko6gGHytt5s6Yj6oOeu5Zm8FoLw==} + mjml-preset-core@4.16.1: + resolution: {integrity: sha512-D7ogih4k31xCvj2u5cATF8r6Z1yTbjMnR+rs19fZ35gXYhl0B8g4cARwXVCu0WcU4vs/3adInAZ8c54NL5ruWA==} - mjml-raw@4.15.3: - resolution: {integrity: sha512-IGyHheOYyRchBLiAEgw3UM11kFNmBSMupu2BDdejC6ZiDhEAdG+tyERlsCwDPYtXanvFpGWULIu3XlsUPc+RZw==} + mjml-raw@4.16.1: + resolution: {integrity: sha512-xQrosP9iNNCrfMnYjJzlzV6fzAysRuv3xuB/JuTuIbS74odvGItxXNnYLUEvwGnslO4ij2J4Era62ExEC3ObNQ==} - mjml-section@4.15.3: - resolution: {integrity: sha512-JfVPRXH++Hd933gmQfG8JXXCBCR6fIzC3DwiYycvanL/aW1cEQ2EnebUfQkt5QzlYjOkJEH+JpccAsq3ln6FZQ==} + mjml-section@4.16.1: + resolution: {integrity: sha512-VxKc+7wEWRsAny9mT464LaaYklz20OUIRDH8XV88LK+8JSd05vcbnEI0eneye6Hly0NIwHARbOI6ssLtNPojIQ==} - mjml-social@4.15.3: - resolution: {integrity: sha512-7sD5FXrESOxpT9Z4Oh36bS6u/geuUrMP1aCg2sjyAwbPcF1aWa2k9OcatQfpRf6pJEhUZ18y6/WBBXmMVmSzXg==} + mjml-social@4.16.1: + resolution: {integrity: sha512-u7k+s7LEY5vB0huJL1aEnkwfJmLX8mln4PDNciO+71/pbi7VRuLuUWqnxHbg7HPP130vJp0tqOrpyIIbxmHlHA==} - mjml-spacer@4.15.3: - resolution: {integrity: sha512-3B7Qj+17EgDdAtZ3NAdMyOwLTX1jfmJuY7gjyhS2HtcZAmppW+cxqHUBwCKfvSRgTQiccmEvtNxaQK+tfyrZqA==} + mjml-spacer@4.16.1: + resolution: {integrity: sha512-HZ9S2Ap3WUf5gYEzs16D8J7wxRG82ReLXd7dM8CSXcfIiqbTUYuApakNlk2cMDOskK9Od1axy8aAirDa7hzv4Q==} - mjml-table@4.15.3: - resolution: {integrity: sha512-FLx7DcRKTdKdcOCbMyBaeudeHaHpwPveRrBm6WyQe3LXx6FfdmOh59i71/16LFQMgBOD3N4/UJkzxLzlTJzMqQ==} + mjml-table@4.16.1: + resolution: {integrity: sha512-JCG/9JFYkx93cSNgxbPBb7KXQjJTa0roEDlKqPC6MkQ3XIy1zCS/jOdZCfhlB2Y9T/9l2AuVBheyK7f7Oftfeg==} - mjml-text@4.15.3: - resolution: {integrity: sha512-+C0hxCmw9kg0XzT6vhE5mFkK6y225nC8UEQcN94K0fBCjPKkM+HqZMwGX205fzdGRi+Bxa55b/VhrIVwdv+8vw==} + mjml-text@4.16.1: + resolution: {integrity: sha512-BmwDXhI+HEe4klEHM9KAXzYxLoUqU97GZI3XMiNdBPSsxKve2x/PSEfRPxEyRaoIpWPsh4HnQBJANzfTgiemSQ==} - mjml-validator@4.15.3: - resolution: {integrity: sha512-Xb72KdqRwjv/qM2rJpV22syyP2N3cRQ9VVDrN6u2FSzLq02buFNxmSPJ7CKhat3PrUNdVHU75KZwOf/tz4UEhA==} + mjml-validator@4.16.1: + resolution: {integrity: sha512-lCePRig7cTLCpkqBk1GAUs+BS3rbO+Nmle+rHLZo5rrHgJJOkozHAJbmaEs9p29KXx0OoUTj+JVMncpUQeCSFA==} - mjml-wrapper@4.15.3: - resolution: {integrity: sha512-ditsCijeHJrmBmObtJmQ18ddLxv5oPyMTdPU8Di8APOnD2zPk7Z4UAuJSl7HXB45oFiivr3MJf4koFzMUSZ6Gg==} + mjml-wrapper@4.16.1: + resolution: {integrity: sha512-OfbKR8dym5vJ4z+n1L0vFfuGfnD8Y1WKrn4rjEuvCWWSE4BeXd/rm4OHy2JKgDo3Wg7kxLkz9ghEO4kFMOKP5g==} - mjml@4.15.3: - resolution: {integrity: sha512-bW2WpJxm6HS+S3Yu6tq1DUPFoTxU9sPviUSmnL7Ua+oVO3WA5ILFWqvujUlz+oeuM+HCwEyMiP5xvKNPENVjYA==} + mjml@4.16.1: + resolution: {integrity: sha512-urrG5JD4vmYNT6kdNHwxeCuiPPR0VFonz4slYQhCBXWS8/KsYxkY2wnYA+vfOLq91aQnMvJzVcUK+ye9z7b51w==} hasBin: true mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true - mlly@1.7.4: - resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} mockdate@3.0.5: resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==} - motion-dom@12.23.12: - resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==} + motion-dom@12.27.1: + resolution: {integrity: sha512-V/53DA2nBqKl9O2PMJleSUb/G0dsMMeZplZwgIQf5+X0bxIu7Q1cTv6DrjvTTGYRm3+7Y5wMlRZ1wT61boU/bQ==} + + motion-utils@12.24.10: + resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} - motion-utils@12.23.6: - resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8402,20 +9905,24 @@ packages: resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} engines: {node: '>= 10.16.0'} + multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - mysql2@3.14.5: - resolution: {integrity: sha512-40hDf8LPUsuuJ2hFq+UgOuPwt2IFLIRDvMv6ez9hKbXeYuZPxDDwiJW7KdknvOsQqKznaKczOT1kELgFkhDvFg==} + mysql2@3.16.1: + resolution: {integrity: sha512-b75qsDB3ieYEzMsT1uRGsztM/sy6vWPY40uPZlVVl8eefAotFCoS7jaDB5DxDNtlW5kdVGd9jptSpkvujNxI2A==} engines: {node: '>= 8.0'} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - named-placeholders@1.1.3: - resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} - engines: {node: '>=12.0.0'} + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} nano-css@5.6.2: resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==} @@ -8428,14 +9935,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.3.2: - resolution: {integrity: sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==} + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -8443,8 +9958,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nestjs-cls@6.0.1: - resolution: {integrity: sha512-FnU8MI5/RKdbNvGmlUMD7nuFs7zUiueNzTjO+sxj7BJbsf7cUmosarppPLXRJ7C1WaZ/gLE9ZAcM6//0q1CQIA==} + nestjs-cls@6.2.0: + resolution: {integrity: sha512-b2Remha7gV5gId3ezjr2tupjqqgYK7/JqjqX6oZ0ZIDFATUggKH1/32+ul2lOe7FepnHasDONDoePuWEE64cug==} engines: {node: '>=18'} peerDependencies: '@nestjs/common': '>= 10 < 12' @@ -8452,13 +9967,13 @@ packages: reflect-metadata: '*' rxjs: '>= 7' - nestjs-pino@4.4.0: - resolution: {integrity: sha512-+GMNlcNWDRrMtlQftfcxN+5pV2C25A4wsYIY7cfRJTMW4b8IFKYReDrG1lUp5LGql9fXemmnVJ2Ww10iIkCZPQ==} + nestjs-pino@4.5.0: + resolution: {integrity: sha512-e54ChJMACSGF8gPYaHsuD07RW7l/OVoV6aI8Hqhpp0ZQ4WA8QY3eewL42JX7Z1U6rV7byNU7bGBV9l6d9V6PDQ==} engines: {node: '>= 14'} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - pino: ^7.5.0 || ^8.0.0 || ^9.0.0 - pino-http: ^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + pino: ^7.5.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + pino-http: ^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 nestjs-typeorm-paginate@4.1.0: @@ -8467,8 +9982,8 @@ packages: '@nestjs/common': ^6.1.1 || ^5.6.2 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 typeorm: ^0.3.0 - next-i18next@15.4.2: - resolution: {integrity: sha512-zgRxWf7kdXtM686ecGIBQL+Bq0+DqAhRlasRZ3vVF0TmrNTWkVhs52n//oU3Fj5O7r/xOKkECDUwfOuXVwTK/g==} + next-i18next@15.4.3: + resolution: {integrity: sha512-ZRmiz72o1Jvh2ZghCUQX1Ua5F/f2W1/Ila/L1ZeKVuSWiH7J4zfUedfDxNBEhj9lajREC7aoJuPXMFtKi2bdIg==} engines: {node: '>=14'} peerDependencies: i18next: '>= 23.7.13' @@ -8476,8 +9991,8 @@ packages: react: '>= 17.0.2' react-i18next: '>= 13.5.0' - next-router-mock@1.0.2: - resolution: {integrity: sha512-9bEA4Sytj1N6xp7UlE4L++QmuU2ZcNTs33Fx8ypNToMlIA7zEotNc0RP0POPP+C3PHWz61U66V6qRDqUg0b3Lg==} + next-router-mock@1.0.5: + resolution: {integrity: sha512-mEndN1KdRlofR1Hf+FvfHogabpx1SQ8cbXYENCm3SA1W5Bj3UlaIW814DA6vg5gmPF6tBw5E1rX2JY4ogN9aaQ==} peerDependencies: next: '>=10.0.0' react: '>=17.0.0' @@ -8488,9 +10003,9 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.4.7: - resolution: {integrity: sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.1.3: + resolution: {integrity: sha512-gthG3TRD+E3/mA0uDQb9lqBmx1zVosq5kIwxNN6+MRNd085GzD+9VXMPUs+GGZCbZ+GDZdODUq4Pm7CTXK6ipw==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -8521,13 +10036,17 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} - node-addon-api@8.3.1: - resolution: {integrity: sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==} + node-addon-api@8.5.0: + resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} engines: {node: ^18 || ^20 || >= 21} node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -8537,6 +10056,10 @@ packages: encoding: optional: true + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -8544,19 +10067,19 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} nodemailer@6.10.1: resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==} engines: {node: '>=6.0.0'} - nodemailer@6.9.13: - resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} + nodemailer@7.0.10: + resolution: {integrity: sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==} engines: {node: '>=6.0.0'} - nodemailer@7.0.6: - resolution: {integrity: sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==} + nodemailer@7.0.12: + resolution: {integrity: sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==} engines: {node: '>=6.0.0'} nopt@7.2.1: @@ -8568,12 +10091,8 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - normalize-url@8.0.1: - resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} + normalize-url@8.1.0: + resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} engines: {node: '>=14.16'} npm-run-path@2.0.2: @@ -8584,9 +10103,18 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + null-loader@4.0.1: + resolution: {integrity: sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + number-flow@0.5.8: resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==} @@ -8608,8 +10136,8 @@ packages: react-router-dom: optional: true - nwsapi@2.2.21: - resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} + nwsapi@2.2.22: + resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -8619,10 +10147,6 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - object-inspect@1.13.3: - resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} - engines: {node: '>= 0.4'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -8651,6 +10175,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -8659,6 +10186,10 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -8666,16 +10197,28 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} - openapi-typescript@7.9.1: - resolution: {integrity: sha512-9gJtoY04mk6iPMbToPjPxEAtfXZ0dTsMZtsgUI8YZta0btPPig9DJFP4jlerQD/7QOwYgb0tl+zLUpDf7vb7VA==} + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openapi-typescript@7.10.1: + resolution: {integrity: sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==} hasBin: true peerDependencies: typescript: ^5.x + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -8684,16 +10227,12 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxc-resolver@11.6.1: - resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==} + oxc-resolver@11.13.1: + resolution: {integrity: sha512-/MS37pbsjfdujmuiM/qONFToT8zjDh78xOhVOPStG7fiZlE0b8od8XOfLhqovL0NnMR0ojumTUWF4LK/U15qDQ==} p-cancelable@3.0.0: resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} @@ -8715,6 +10254,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -8723,6 +10266,22 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@6.2.1: + resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} + engines: {node: '>=16.17'} + p-timeout@3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} @@ -8738,16 +10297,26 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} param-case@2.1.1: resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -8756,8 +10325,11 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} - parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -8769,6 +10341,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + passport-custom@1.1.1: resolution: {integrity: sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==} engines: {node: '>= 0.10.0'} @@ -8792,10 +10367,17 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + path-key@2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} @@ -8811,13 +10393,21 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.0: - resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -8832,14 +10422,6 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - peek-readable@5.4.2: - resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} - engines: {node: '>=14.16'} - - peek-readable@7.0.0: - resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==} - engines: {node: '>=18'} - pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -8854,6 +10436,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -8861,49 +10447,52 @@ packages: pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} - pino-http@10.5.0: - resolution: {integrity: sha512-hD91XjgaKkSsdn8P7LaebrNzhGTdB086W3pyPihX0EzGPjq5uBJBXo4N5guqNaK6mUjg9aubMF7wDViYek9dRA==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-http@11.0.0: + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} - pino-pretty@13.1.1: - resolution: {integrity: sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==} + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} hasBin: true pino-std-serializers@7.0.0: resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} - pino@9.7.0: - resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + pino@10.1.0: + resolution: {integrity: sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==} hasBin: true - pino@9.9.4: - resolution: {integrity: sha512-d1XorUQ7sSKqVcYdXuEYs2h1LKxejSorMEJ76XoZ0pPDf8VzJMe7GlPXpMBZeQ9gE4ZPIp5uGD+5Nw7scxiigg==} + pino@10.2.0: + resolution: {integrity: sha512-NFnZqUliT+OHkRXVSf8vdOr13N1wv31hRryVjqbreVh/SDCNaI6mnRDDq89HVRCbem1SAl7yj04OANeqP0nT6A==} hasBin: true - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - piscina@4.7.0: - resolution: {integrity: sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==} + piscina@4.9.2: + resolution: {integrity: sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==} pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.55.0: - resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} hasBin: true - playwright@1.55.0: - resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==} + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} engines: {node: '>=18'} hasBin: true @@ -8911,66 +10500,412 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-import@16.1.1: - resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} - engines: {node: '>=18.0.0'} + postcss-attribute-case-insensitive@7.0.1: + resolution: {integrity: sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==} + engines: {node: '>=18'} peerDependencies: - postcss: ^8.0.0 + postcss: ^8.4 - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} + postcss-calc@9.0.1: + resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} + engines: {node: ^14 || ^16 || >=18.0} peerDependencies: - postcss: ^8.4.21 + postcss: ^8.2.2 - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} + postcss-clamp@4.1.0: + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true + postcss: ^8.4.6 - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} + postcss-color-functional-notation@7.0.12: + resolution: {integrity: sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==} + engines: {node: '>=18'} peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true + postcss: ^8.4 - postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} + postcss-color-hex-alpha@10.0.0: + resolution: {integrity: sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==} + engines: {node: '>=18'} peerDependencies: - postcss: ^8.2.14 + postcss: ^8.4 - postcss-nesting@13.0.2: - resolution: {integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==} + postcss-color-rebeccapurple@10.0.0: + resolution: {integrity: sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-colormin@6.1.0: + resolution: {integrity: sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-convert-values@6.1.0: + resolution: {integrity: sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-custom-media@11.0.6: + resolution: {integrity: sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-custom-properties@14.0.6: + resolution: {integrity: sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-custom-selectors@8.0.5: + resolution: {integrity: sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-dir-pseudo-class@9.0.1: + resolution: {integrity: sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-discard-comments@6.0.2: + resolution: {integrity: sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-duplicates@6.0.3: + resolution: {integrity: sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-empty@6.0.3: + resolution: {integrity: sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-overridden@6.0.2: + resolution: {integrity: sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-unused@6.0.5: + resolution: {integrity: sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-double-position-gradients@6.0.4: + resolution: {integrity: sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-focus-visible@10.0.1: + resolution: {integrity: sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-focus-within@9.0.1: + resolution: {integrity: sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-font-variant@5.0.0: + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} + peerDependencies: + postcss: ^8.1.0 + + postcss-gap-properties@6.0.0: + resolution: {integrity: sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-image-set-function@7.0.0: + resolution: {integrity: sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-import@16.1.1: + resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-js@5.0.3: + resolution: {integrity: sha512-yqxfMZ2NKo8MH0xcj6Yb1sos9Vk2aNzVi0i6k0nWH0LaLQQ1lke9DGWDMa80+tzHk+tLzfKa3pepOFcPSM6Yow==} + engines: {node: ^20 || ^22 || >= 24} + peerDependencies: + postcss: ^8.4.21 + + postcss-lab-function@7.0.12: + resolution: {integrity: sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-loader@7.3.4: + resolution: {integrity: sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-logical@8.1.0: + resolution: {integrity: sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-merge-idents@6.0.3: + resolution: {integrity: sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-longhand@6.0.5: + resolution: {integrity: sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-rules@6.1.1: + resolution: {integrity: sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-font-values@6.1.0: + resolution: {integrity: sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-gradients@6.0.3: + resolution: {integrity: sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-params@6.1.0: + resolution: {integrity: sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-selectors@6.0.4: + resolution: {integrity: sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-nesting@13.0.2: + resolution: {integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-nesting@14.0.0: + resolution: {integrity: sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + + postcss-normalize-charset@6.0.2: + resolution: {integrity: sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-display-values@6.0.2: + resolution: {integrity: sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-positions@6.0.2: + resolution: {integrity: sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-repeat-style@6.0.2: + resolution: {integrity: sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-string@6.0.2: + resolution: {integrity: sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-timing-functions@6.0.2: + resolution: {integrity: sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-unicode@6.1.0: + resolution: {integrity: sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-url@6.0.2: + resolution: {integrity: sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-whitespace@6.0.2: + resolution: {integrity: sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-opacity-percentage@3.0.0: + resolution: {integrity: sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-ordered-values@6.0.2: + resolution: {integrity: sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-overflow-shorthand@6.0.0: + resolution: {integrity: sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-page-break@3.0.4: + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} + peerDependencies: + postcss: ^8 + + postcss-place@10.0.0: + resolution: {integrity: sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-preset-env@10.4.0: + resolution: {integrity: sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-pseudo-class-any-link@10.0.1: + resolution: {integrity: sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-reduce-idents@6.0.3: + resolution: {integrity: sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-initial@6.1.0: + resolution: {integrity: sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-transforms@6.0.2: + resolution: {integrity: sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-replace-overflow-wrap@4.0.0: + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} + peerDependencies: + postcss: ^8.0.3 + + postcss-selector-not@8.0.1: + resolution: {integrity: sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 @@ -8983,9 +10918,37 @@ packages: resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} engines: {node: '>=4'} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-sort-media-queries@5.2.0: + resolution: {integrity: sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.4.23 + + postcss-svgo@6.0.3: + resolution: {integrity: sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==} + engines: {node: ^14 || ^16 || >= 18} + peerDependencies: + postcss: ^8.4.31 + + postcss-unique-selectors@6.0.4: + resolution: {integrity: sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss-zindex@6.0.2: + resolution: {integrity: sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -8998,9 +10961,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-tailwindcss@0.6.14: - resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} - engines: {node: '>=14.21.3'} + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' '@prettier/plugin-hermes': '*' @@ -9012,14 +10975,12 @@ packages: prettier: ^3.0 prettier-plugin-astro: '*' prettier-plugin-css-order: '*' - prettier-plugin-import-sort: '*' prettier-plugin-jsdoc: '*' prettier-plugin-marko: '*' prettier-plugin-multiline-arrays: '*' prettier-plugin-organize-attributes: '*' prettier-plugin-organize-imports: '*' prettier-plugin-sort-imports: '*' - prettier-plugin-style-order: '*' prettier-plugin-svelte: '*' peerDependenciesMeta: '@ianvs/prettier-plugin-sort-imports': @@ -9040,8 +11001,6 @@ packages: optional: true prettier-plugin-css-order: optional: true - prettier-plugin-import-sort: - optional: true prettier-plugin-jsdoc: optional: true prettier-plugin-marko: @@ -9054,28 +11013,42 @@ packages: optional: true prettier-plugin-sort-imports: optional: true - prettier-plugin-style-order: - optional: true prettier-plugin-svelte: optional: true - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true + pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-format@30.0.5: - resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} + pretty-format@30.2.0: + resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - preview-email@3.0.20: - resolution: {integrity: sha512-QbAokW2F3p0thQfp2WTZ0rBy+IZuCnf9gIUCLffr+8hq85esq6pzCA7S0eUdD6oTmtKROqoNeH2rXZWrRow7EA==} + pretty-time@1.1.0: + resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} + engines: {node: '>=4'} + + preview-email@3.1.0: + resolution: {integrity: sha512-ZtV1YrwscEjlrUzYrTSs6Nwo49JM3pXLM4fFOBSC3wSni+bxaWlw9/Qgk75PZO8M7cX2EybmL2iwvaV3vkAttw==} engines: {node: '>=14'} + prism-react-renderer@2.4.1: + resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} + peerDependencies: + react: '>=16.0.0' + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -9092,9 +11065,19 @@ packages: promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -9141,8 +11124,8 @@ packages: pug@3.0.3: resolution: {integrity: sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==} - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} @@ -9152,19 +11135,24 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pupa@3.3.0: + resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} + engines: {node: '>=12.20'} + pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - queue-tick@1.0.1: - resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} - quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -9175,14 +11163,22 @@ packages: randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + range-parser@1.2.0: + resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} + engines: {node: '>= 0.6'} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -9199,21 +11195,24 @@ packages: date-fns: ^2.28.0 || ^3.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom@19.1.1: - resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: - react: ^19.1.1 + react: ^19.2.3 + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hook-form@7.62.0: - resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} + react-hook-form@7.71.1: + resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@15.7.3: - resolution: {integrity: sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==} + react-i18next@16.5.3: + resolution: {integrity: sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw==} peerDependencies: - i18next: '>= 25.4.1' + i18next: '>= 25.6.2' react: '>= 16.8.0' react-dom: '*' react-native: '*' @@ -9235,6 +11234,31 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-json-view-lite@2.5.0: + resolution: {integrity: sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + react-loadable-ssr-addon-v5-slorber@1.0.1: + resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} + engines: {node: '>=10.13.0'} + peerDependencies: + react-loadable: '*' + webpack: '>=4.41.1 || 5.x' + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -9245,8 +11269,8 @@ packages: '@types/react': optional: true - react-remove-scroll@2.6.3: - resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} engines: {node: '>=10'} peerDependencies: '@types/react': '*' @@ -9255,14 +11279,24 @@ packages: '@types/react': optional: true - react-select@5.10.2: - resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + react-router-config@5.1.1: + resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: '>=15' + react-router: '>=5' + + react-router-dom@5.3.4: + resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} + peerDependencies: + react: '>=15' + + react-router@5.3.4: + resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + peerDependencies: + react: '>=15' - react-smooth@4.0.4: - resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -9295,8 +11329,8 @@ packages: react: '*' react-dom: '*' - react@19.1.1: - resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -9324,20 +11358,40 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - recharts-scale@0.4.5: - resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - - recharts@2.15.4: - resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} - engines: {node: '>=14'} + recharts@3.5.0: + resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==} + engines: {node: '>=18'} peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -9345,35 +11399,74 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerate-unicode-properties@10.2.0: - resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} engines: {node: '>=4'} regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - regexpu-core@6.2.0: - resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + regjsgen@0.8.0: resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} - regjsparser@0.12.0: - resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + relateurl@0.2.7: resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} engines: {node: '>= 0.10'} + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + + remark-emoji@4.0.1: + resolution: {integrity: sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -9386,6 +11479,15 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-like@0.1.2: + resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -9404,8 +11506,11 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true @@ -9425,6 +11530,10 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -9437,8 +11546,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.34.8: - resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==} + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -9452,10 +11561,19 @@ packages: rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + rtlcss@4.3.0: + resolution: {integrity: sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==} + engines: {node: '>=12.0.0'} + hasBin: true + run-applescript@3.2.0: resolution: {integrity: sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==} engines: {node: '>=4'} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -9493,6 +11611,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + saxes@5.0.1: resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} engines: {node: '>=10'} @@ -9501,26 +11622,36 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + schema-dts@1.1.5: + resolution: {integrity: sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==} schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} - schema-utils@4.3.2: - resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} screenfull@5.2.0: resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} engines: {node: '>=0.10.0'} + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - secure-json-parse@4.0.0: - resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} seek-bzip@2.0.0: resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==} @@ -9529,6 +11660,17 @@ packages: selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + + semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + semver-regex@4.0.5: resolution: {integrity: sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==} engines: {node: '>=12'} @@ -9545,15 +11687,14 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.2: - resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} @@ -9565,12 +11706,23 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serve-handler@6.1.6: + resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} + + serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -9591,15 +11743,26 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sha.js@2.4.11: - resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} hasBin: true - sharp@0.34.3: - resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@1.2.0: @@ -9618,6 +11781,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} @@ -9644,19 +11811,39 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sitemap@7.1.2: + resolution: {integrity: sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==} + engines: {node: '>=12.0.0', npm: '>=5.6.0'} + hasBin: true + + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + slick@1.12.2: resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} @@ -9666,6 +11853,10 @@ packages: react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + sort-css-media-queries@2.2.0: + resolution: {integrity: sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==} + engines: {node: '>= 6.3.0'} + sort-keys-length@1.0.1: resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} engines: {node: '>=0.10.0'} @@ -9700,10 +11891,19 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - deprecated: The work that was done in this beta branch won't be included in future versions + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + spdy-transport@3.0.0: + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + + spdy@4.0.2: + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} @@ -9712,14 +11912,18 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-highlight@6.0.0: - resolution: {integrity: sha512-+fLpbAbWkQ+d0JEchJT/NrRRXbYRNbG15gFpANx73EwxQB1PRjj+k/OI0GTU0J63g8ikGkJECQp9z8XEJZvPRw==} + sql-highlight@6.1.0: + resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} engines: {node: '>=14'} sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} + srcset@4.0.0: + resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} + engines: {node: '>=12'} + stack-chain@1.3.7: resolution: {integrity: sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==} @@ -9739,10 +11943,21 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -9751,8 +11966,8 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - streamx@2.20.2: - resolution: {integrity: sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -9795,14 +12010,25 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -9834,20 +12060,22 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-json-comments@5.0.2: - resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} strnum@2.1.1: resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} - strtok3@10.2.2: - resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==} + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} - strtok3@9.1.1: - resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==} - engines: {node: '>=16'} + style-to-js@1.1.19: + resolution: {integrity: sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==} + + style-to-object@1.0.12: + resolution: {integrity: sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==} styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} @@ -9862,27 +12090,33 @@ packages: babel-plugin-macros: optional: true + stylehacks@6.1.1: + resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - stylis@4.3.4: - resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true - superagent@10.2.3: - resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} - supertest@7.1.4: - resolution: {integrity: sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==} + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} - supports-color@10.1.0: - resolution: {integrity: sha512-GBuewsPrhJPftT+fqDa9oI/zc5HNsG9nREqwzoSFDOIqf0NggOZbHQj2TE1P1CDJK8ZogFnlZY9hWoUiur7I/A==} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} supports-color@7.2.0: @@ -9905,11 +12139,11 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - swagger-ui-dist@5.21.0: - resolution: {integrity: sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==} + swagger-ui-dist@5.31.0: + resolution: {integrity: sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==} - swiper@11.2.10: - resolution: {integrity: sha512-RMeVUUjTQH+6N3ckimK93oxz6Sn5la4aDlgPzB+rBrG/smPdCTicXyhxa+woIpopz+jewEloiEE3lKo1h9w2YQ==} + swiper@12.0.3: + resolution: {integrity: sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==} engines: {node: '>= 4.7.0'} symbol-observable@4.0.0: @@ -9923,21 +12157,21 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} - tailwind-merge@3.3.1: - resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} tailwind-scrollbar-hide@4.0.0: resolution: {integrity: sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ==} peerDependencies: tailwindcss: '>=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20' - tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} tar-stream@2.2.0: @@ -9966,8 +12200,24 @@ packages: uglify-js: optional: true - terser@5.39.0: - resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} + terser-webpack-plugin@5.3.16: + resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.44.1: + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} engines: {node: '>=10'} hasBin: true @@ -9975,8 +12225,8 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - text-decoder@1.2.1: - resolution: {integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} @@ -9985,9 +12235,19 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@2.5.0: + resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + throttle-debounce@3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} @@ -9995,22 +12255,28 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.12: - resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} - engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} - tlds@1.252.0: - resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} + tlds@1.261.0: + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} hasBin: true tldts-core@6.1.86: @@ -10020,17 +12286,17 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - - tmp@0.2.3: - resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -10049,10 +12315,17 @@ packages: token-stream@1.0.0: resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} - token-types@6.0.0: - resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} + token-types@6.1.1: + resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} engines: {node: '>=14.16'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -10060,9 +12333,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} @@ -10070,10 +12340,22 @@ packages: traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -10086,8 +12368,8 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-jest@29.4.1: - resolution: {integrity: sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==} + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -10148,14 +12430,11 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.0: - resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -10173,38 +12452,38 @@ packages: typescript: optional: true - turbo-darwin-64@2.5.6: - resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==} + turbo-darwin-64@2.7.5: + resolution: {integrity: sha512-nN3wfLLj4OES/7awYyyM7fkU8U8sAFxsXau2bYJwAWi6T09jd87DgHD8N31zXaJ7LcpyppHWPRI2Ov9MuZEwnQ==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.5.6: - resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==} + turbo-darwin-arm64@2.7.5: + resolution: {integrity: sha512-wCoDHMiTf3FgLAbZHDDx/unNNonSGhsF5AbbYODbxnpYyoKDpEYacUEPjZD895vDhNvYCH0Nnk24YsP4n/cD6g==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.5.6: - resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==} + turbo-linux-64@2.7.5: + resolution: {integrity: sha512-KKPvhOmJMmzWj/yjeO4LywkQ85vOJyhru7AZk/+c4B6OUh/odQ++SiIJBSbTG2lm1CuV5gV5vXZnf/2AMlu3Zg==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.5.6: - resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==} + turbo-linux-arm64@2.7.5: + resolution: {integrity: sha512-8PIva4L6BQhiPikUTds9lSFSHXVDAsEvV6QUlgwPsXrtXVQMVi6Sv9p+IxtlWQFvGkdYJUgX9GnK2rC030Xcmw==} cpu: [arm64] os: [linux] - turbo-windows-64@2.5.6: - resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==} + turbo-windows-64@2.7.5: + resolution: {integrity: sha512-rupskv/mkIUgQXzX/wUiK00mKMorQcK8yzhGFha/D5lm05FEnLx8dsip6rWzMcVpvh+4GUMA56PgtnOgpel2AA==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.5.6: - resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==} + turbo-windows-arm64@2.7.5: + resolution: {integrity: sha512-G377Gxn6P42RnCzfMyDvsqQV7j69kVHKlhz9J4RhtJOB5+DyY4yYh/w0oTIxZQ4JRMmhjwLu3w9zncMoQ6nNDw==} cpu: [arm64] os: [win32] - turbo@2.5.6: - resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==} + turbo@2.7.5: + resolution: {integrity: sha512-7Imdmg37joOloTnj+DPrab9hIaQcDdJ5RwSzcauo/wMOSAgO+A/I/8b3hsGGs6PWQz70m/jkPgdqWsfNKtwwDQ==} hasBin: true type-check@0.4.0: @@ -10223,6 +12502,14 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -10251,6 +12538,9 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -10266,24 +12556,23 @@ packages: reflect-metadata: '>= 0.1.12' typeorm: '>= 0.2.8' - typeorm@0.3.26: - resolution: {integrity: sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==} + typeorm@0.3.28: + resolution: {integrity: sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: - '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 + '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 '@sap/hana-client': ^2.14.22 better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 ioredis: ^5.0.4 mongodb: ^5.8.0 || ^6.0.0 - mssql: ^9.1.1 || ^10.0.1 || ^11.0.1 + mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0 mysql2: ^2.2.5 || ^3.0.1 oracledb: ^6.3.0 pg: ^8.5.1 pg-native: ^3.0.0 pg-query-stream: ^4.0.0 redis: ^3.1.1 || ^4.0.0 || ^5.0.14 - reflect-metadata: ^0.1.14 || ^0.2.0 sql.js: ^1.4.0 sqlite3: ^5.0.3 ts-node: ^10.7.0 @@ -10322,15 +12611,15 @@ packages: typeorm-aurora-data-api-driver: optional: true - typescript-eslint@8.43.0: - resolution: {integrity: sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==} + typescript-eslint@8.46.3: + resolution: {integrity: sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -10340,8 +12629,8 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - uglify-js@3.18.0: - resolution: {integrity: sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} hasBin: true @@ -10349,8 +12638,8 @@ packages: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} - uint8array-extras@1.4.0: - resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} unbox-primitive@1.1.0: @@ -10360,25 +12649,54 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + unicode-match-property-ecmascript@2.0.0: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} - unicode-match-property-value-ecmascript@2.2.0: - resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} engines: {node: '>=4'} - unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10393,12 +12711,22 @@ packages: unzipper@0.10.14: resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + update-notifier@6.0.2: + resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} + engines: {node: '>=14.16'} + upper-case@1.1.3: resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} @@ -10408,6 +12736,16 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-loader@4.1.1: + resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + file-loader: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + file-loader: + optional: true + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -10418,8 +12756,8 @@ packages: '@types/react': optional: true - use-isomorphic-layout-effect@1.2.0: - resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==} + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} peerDependencies: '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -10437,14 +12775,21 @@ packages: '@types/react': optional: true - use-sync-external-store@1.4.0: - resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -10453,6 +12798,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -10472,16 +12821,28 @@ packages: resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} engines: {node: '>=10'} - validator@13.12.0: - resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + validator@13.15.20: + resolution: {integrity: sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==} engines: {node: '>= 0.10'} + value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - victory-vendor@36.9.2: - resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} @@ -10494,13 +12855,19 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.4.2: - resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} + wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-resource-inliner@6.0.1: resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} engines: {node: '>=10.0.0'} @@ -10508,13 +12875,45 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-bundle-analyzer@4.10.2: + resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} + engines: {node: '>= 10.13.0'} + hasBin: true + + webpack-dev-middleware@7.4.5: + resolution: {integrity: sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + + webpack-dev-server@5.2.2: + resolution: {integrity: sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==} + engines: {node: '>= 18.12.0'} + hasBin: true + peerDependencies: + webpack: ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-merge@6.0.1: + resolution: {integrity: sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==} + engines: {node: '>=18.0.0'} + webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -10533,9 +12932,34 @@ packages: webpack-cli: optional: true + webpack@5.104.1: + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + webpackbar@6.0.1: + resolution: {integrity: sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + webpack: 3 || 4 || 5 + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -10548,9 +12972,6 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -10563,10 +12984,6 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} - engines: {node: '>= 0.4'} - which-typed-array@1.1.19: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} @@ -10584,6 +13001,13 @@ packages: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + with@7.0.2: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} @@ -10610,12 +13034,27 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -10626,6 +13065,18 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -10633,8 +13084,8 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xregexp@5.1.1: - resolution: {integrity: sha512-fKXeVorD+CzWvFs7VBuKTYIW63YD1e1osxwQ8caZ6o1jg6pDAbABDG54LCIq0j5cy7PjRvGIq6sef9DYPXpncg==} + xregexp@5.1.2: + resolution: {integrity: sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==} xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} @@ -10654,9 +13105,9 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.7.0: - resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} - engines: {node: '>= 14'} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@21.1.1: @@ -10679,28 +13130,38 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors-cjs@2.1.2: - resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} - zod-validation-error@3.4.0: - resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} + zod-validation-error@3.5.4: + resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.24.4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} peerDependencies: - zod: ^3.18.0 + zod: ^3.25.0 || ^4.0.0 - zod@3.24.4: - resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.5: - resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} - zustand@5.0.8: - resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + zustand@5.0.10: + resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -10717,57 +13178,187 @@ packages: use-sync-external-store: optional: true -snapshots: + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - '@adobe/css-tools@4.4.0': {} +snapshots: - '@alloc/quick-lru@5.2.0': {} + '@adobe/css-tools@4.4.4': {} - '@angular-devkit/core@19.2.15(chokidar@4.0.3)': + '@algolia/abtesting@1.9.0': dependencies: - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - jsonc-parser: 3.3.1 - picomatch: 4.0.2 - rxjs: 7.8.1 - source-map: 0.7.4 - optionalDependencies: - chokidar: 4.0.3 + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 - '@angular-devkit/schematics-cli@19.2.15(@types/node@22.18.1)(chokidar@4.0.3)': + '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0)(search-insights@2.17.3)': dependencies: - '@angular-devkit/core': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - '@inquirer/prompts': 7.3.2(@types/node@22.18.1) - ansi-colors: 4.1.3 - symbol-observable: 4.0.0 - yargs-parser: 21.1.1 + '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0) transitivePeerDependencies: - - '@types/node' - - chokidar + - '@algolia/client-search' + - algoliasearch + - search-insights - '@angular-devkit/schematics@19.2.15(chokidar@4.0.3)': + '@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0)(search-insights@2.17.3)': dependencies: - '@angular-devkit/core': 19.2.15(chokidar@4.0.3) - jsonc-parser: 3.3.1 - magic-string: 0.30.17 - ora: 5.4.1 - rxjs: 7.8.1 + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0) + search-insights: 2.17.3 transitivePeerDependencies: - - chokidar + - '@algolia/client-search' + - algoliasearch - '@ark/schema@0.46.0': + '@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0)': dependencies: - '@ark/util': 0.46.0 - optional: true + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0) + '@algolia/client-search': 5.43.0 + algoliasearch: 5.43.0 - '@ark/util@0.46.0': - optional: true + '@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0)': + dependencies: + '@algolia/client-search': 5.43.0 + algoliasearch: 5.43.0 + + '@algolia/client-abtesting@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/client-analytics@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/client-common@5.43.0': {} + + '@algolia/client-insights@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/client-personalization@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/client-query-suggestions@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/client-search@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/events@4.0.1': {} + + '@algolia/ingestion@1.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/monitoring@1.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/recommend@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + + '@algolia/requester-browser-xhr@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + + '@algolia/requester-fetch@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + + '@algolia/requester-node-http@5.43.0': + dependencies: + '@algolia/client-common': 5.43.0 + + '@alloc/quick-lru@5.2.0': {} + + '@angular-devkit/core@19.2.17(chokidar@4.0.3)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + jsonc-parser: 3.3.1 + picomatch: 4.0.2 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.19(chokidar@4.0.3)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + jsonc-parser: 3.3.1 + picomatch: 4.0.2 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.19(@types/node@24.10.8)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@24.10.8) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.17(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.19(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 @@ -10775,21 +13366,21 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.862.0 + '@aws-sdk/types': 3.969.0 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.862.0 + '@aws-sdk/types': 3.969.0 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-locate-window': 3.723.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-locate-window': 3.893.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -10798,15 +13389,15 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-locate-window': 3.723.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-locate-window': 3.893.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.862.0 + '@aws-sdk/types': 3.969.0 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -10815,1699 +13406,1722 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.862.0 + '@aws-sdk/types': 3.969.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.884.0': + '@aws-sdk/client-s3@3.971.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.883.0 - '@aws-sdk/credential-provider-node': 3.883.0 - '@aws-sdk/middleware-bucket-endpoint': 3.873.0 - '@aws-sdk/middleware-expect-continue': 3.873.0 - '@aws-sdk/middleware-flexible-checksums': 3.883.0 - '@aws-sdk/middleware-host-header': 3.873.0 - '@aws-sdk/middleware-location-constraint': 3.873.0 - '@aws-sdk/middleware-logger': 3.876.0 - '@aws-sdk/middleware-recursion-detection': 3.873.0 - '@aws-sdk/middleware-sdk-s3': 3.883.0 - '@aws-sdk/middleware-ssec': 3.873.0 - '@aws-sdk/middleware-user-agent': 3.883.0 - '@aws-sdk/region-config-resolver': 3.873.0 - '@aws-sdk/signature-v4-multi-region': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.879.0 - '@aws-sdk/util-user-agent-browser': 3.873.0 - '@aws-sdk/util-user-agent-node': 3.883.0 - '@aws-sdk/xml-builder': 3.873.0 - '@smithy/config-resolver': 4.2.0 - '@smithy/core': 3.10.0 - '@smithy/eventstream-serde-browser': 4.0.5 - '@smithy/eventstream-serde-config-resolver': 4.1.3 - '@smithy/eventstream-serde-node': 4.0.5 - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/hash-blob-browser': 4.0.5 - '@smithy/hash-node': 4.0.5 - '@smithy/hash-stream-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/md5-js': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/middleware-retry': 4.2.0 - '@smithy/middleware-serde': 4.1.0 - '@smithy/middleware-stack': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.1.0 - '@smithy/util-defaults-mode-node': 4.1.0 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-retry': 4.1.0 - '@smithy/util-stream': 4.3.0 - '@smithy/util-utf8': 4.1.0 - '@smithy/util-waiter': 4.0.7 - '@types/uuid': 9.0.8 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/credential-provider-node': 3.971.0 + '@aws-sdk/middleware-bucket-endpoint': 3.969.0 + '@aws-sdk/middleware-expect-continue': 3.969.0 + '@aws-sdk/middleware-flexible-checksums': 3.971.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-location-constraint': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-sdk-s3': 3.970.0 + '@aws-sdk/middleware-ssec': 3.971.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/signature-v4-multi-region': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.6 + '@smithy/eventstream-serde-browser': 4.2.8 + '@smithy/eventstream-serde-config-resolver': 4.3.8 + '@smithy/eventstream-serde-node': 4.2.8 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-blob-browser': 4.2.9 + '@smithy/hash-node': 4.2.8 + '@smithy/hash-stream-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/md5-js': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/middleware-retry': 4.4.23 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.22 + '@smithy/util-defaults-mode-node': 4.2.25 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + '@smithy/util-waiter': 4.2.8 tslib: 2.8.1 - uuid: 9.0.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sesv2@3.864.0': + '@aws-sdk/client-sesv2@3.925.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/credential-provider-node': 3.864.0 - '@aws-sdk/middleware-host-header': 3.862.0 - '@aws-sdk/middleware-logger': 3.862.0 - '@aws-sdk/middleware-recursion-detection': 3.862.0 - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/region-config-resolver': 3.862.0 - '@aws-sdk/signature-v4-multi-region': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@aws-sdk/util-user-agent-browser': 3.862.0 - '@aws-sdk/util-user-agent-node': 3.864.0 - '@smithy/config-resolver': 4.1.5 - '@smithy/core': 3.8.0 - '@smithy/fetch-http-handler': 5.1.1 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.1.18 - '@smithy/middleware-retry': 4.1.19 - '@smithy/middleware-serde': 4.0.9 - '@smithy/middleware-stack': 4.0.5 - '@smithy/node-config-provider': 4.1.4 - '@smithy/node-http-handler': 4.1.1 - '@smithy/protocol-http': 5.1.3 - '@smithy/smithy-client': 4.4.10 - '@smithy/types': 4.3.2 - '@smithy/url-parser': 4.0.5 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.26 - '@smithy/util-defaults-mode-node': 4.0.26 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.0.5 - '@smithy/util-retry': 4.0.7 - '@smithy/util-utf8': 4.0.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/credential-provider-node': 3.925.0 + '@aws-sdk/middleware-host-header': 3.922.0 + '@aws-sdk/middleware-logger': 3.922.0 + '@aws-sdk/middleware-recursion-detection': 3.922.0 + '@aws-sdk/middleware-user-agent': 3.922.0 + '@aws-sdk/region-config-resolver': 3.925.0 + '@aws-sdk/signature-v4-multi-region': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@aws-sdk/util-endpoints': 3.922.0 + '@aws-sdk/util-user-agent-browser': 3.922.0 + '@aws-sdk/util-user-agent-node': 3.922.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.6 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/middleware-retry': 4.4.23 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.22 + '@smithy/util-defaults-mode-node': 4.2.25 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.864.0': + '@aws-sdk/client-sso@3.925.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/middleware-host-header': 3.862.0 - '@aws-sdk/middleware-logger': 3.862.0 - '@aws-sdk/middleware-recursion-detection': 3.862.0 - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/region-config-resolver': 3.862.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@aws-sdk/util-user-agent-browser': 3.862.0 - '@aws-sdk/util-user-agent-node': 3.864.0 - '@smithy/config-resolver': 4.2.0 - '@smithy/core': 3.10.0 - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/middleware-retry': 4.2.0 - '@smithy/middleware-serde': 4.1.0 - '@smithy/middleware-stack': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.1.0 - '@smithy/util-defaults-mode-node': 4.1.0 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-retry': 4.1.0 - '@smithy/util-utf8': 4.1.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/middleware-host-header': 3.922.0 + '@aws-sdk/middleware-logger': 3.922.0 + '@aws-sdk/middleware-recursion-detection': 3.922.0 + '@aws-sdk/middleware-user-agent': 3.922.0 + '@aws-sdk/region-config-resolver': 3.925.0 + '@aws-sdk/types': 3.922.0 + '@aws-sdk/util-endpoints': 3.922.0 + '@aws-sdk/util-user-agent-browser': 3.922.0 + '@aws-sdk/util-user-agent-node': 3.922.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.6 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/middleware-retry': 4.4.23 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.22 + '@smithy/util-defaults-mode-node': 4.2.25 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.883.0': + '@aws-sdk/client-sso@3.971.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.883.0 - '@aws-sdk/middleware-host-header': 3.873.0 - '@aws-sdk/middleware-logger': 3.876.0 - '@aws-sdk/middleware-recursion-detection': 3.873.0 - '@aws-sdk/middleware-user-agent': 3.883.0 - '@aws-sdk/region-config-resolver': 3.873.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.879.0 - '@aws-sdk/util-user-agent-browser': 3.873.0 - '@aws-sdk/util-user-agent-node': 3.883.0 - '@smithy/config-resolver': 4.2.0 - '@smithy/core': 3.10.0 - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/middleware-retry': 4.2.0 - '@smithy/middleware-serde': 4.1.0 - '@smithy/middleware-stack': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.1.0 - '@smithy/util-defaults-mode-node': 4.1.0 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-retry': 4.1.0 - '@smithy/util-utf8': 4.1.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.6 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/middleware-retry': 4.4.23 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.22 + '@smithy/util-defaults-mode-node': 4.2.25 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.864.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@aws-sdk/xml-builder': 3.862.0 - '@smithy/core': 3.10.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/property-provider': 4.0.5 - '@smithy/protocol-http': 5.2.0 - '@smithy/signature-v4': 5.1.3 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-utf8': 4.1.0 - fast-xml-parser: 5.2.5 + '@aws-sdk/core@3.922.0': + dependencies: + '@aws-sdk/types': 3.922.0 + '@aws-sdk/xml-builder': 3.921.0 + '@smithy/core': 3.20.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/core@3.883.0': - dependencies: - '@aws-sdk/types': 3.862.0 - '@aws-sdk/xml-builder': 3.873.0 - '@smithy/core': 3.10.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/property-provider': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/signature-v4': 5.1.3 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-utf8': 4.1.0 - fast-xml-parser: 5.2.5 + '@aws-sdk/core@3.970.0': + dependencies: + '@aws-sdk/types': 3.969.0 + '@aws-sdk/xml-builder': 3.969.0 + '@smithy/core': 3.20.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.864.0': + '@aws-sdk/crc64-nvme@3.969.0': dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.883.0': + '@aws-sdk/credential-provider-env@3.922.0': dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.970.0': + dependencies: + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.922.0': + dependencies: + '@aws-sdk/core': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/property-provider': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-stream': 4.3.0 + '@aws-sdk/credential-provider-http@3.970.0': + dependencies: + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.883.0': - dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/property-provider': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-stream': 4.3.0 + '@aws-sdk/credential-provider-ini@3.925.0': + dependencies: + '@aws-sdk/core': 3.922.0 + '@aws-sdk/credential-provider-env': 3.922.0 + '@aws-sdk/credential-provider-http': 3.922.0 + '@aws-sdk/credential-provider-process': 3.922.0 + '@aws-sdk/credential-provider-sso': 3.925.0 + '@aws-sdk/credential-provider-web-identity': 3.925.0 + '@aws-sdk/nested-clients': 3.925.0 + '@aws-sdk/types': 3.922.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt - '@aws-sdk/credential-provider-ini@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/credential-provider-env': 3.864.0 - '@aws-sdk/credential-provider-http': 3.864.0 - '@aws-sdk/credential-provider-process': 3.864.0 - '@aws-sdk/credential-provider-sso': 3.864.0 - '@aws-sdk/credential-provider-web-identity': 3.864.0 - '@aws-sdk/nested-clients': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.1.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/credential-provider-ini@3.971.0': + dependencies: + '@aws-sdk/core': 3.970.0 + '@aws-sdk/credential-provider-env': 3.970.0 + '@aws-sdk/credential-provider-http': 3.970.0 + '@aws-sdk/credential-provider-login': 3.971.0 + '@aws-sdk/credential-provider-process': 3.970.0 + '@aws-sdk/credential-provider-sso': 3.971.0 + '@aws-sdk/credential-provider-web-identity': 3.971.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-ini@3.883.0': - dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/credential-provider-env': 3.883.0 - '@aws-sdk/credential-provider-http': 3.883.0 - '@aws-sdk/credential-provider-process': 3.883.0 - '@aws-sdk/credential-provider-sso': 3.883.0 - '@aws-sdk/credential-provider-web-identity': 3.883.0 - '@aws-sdk/nested-clients': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.1.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/credential-provider-login@3.971.0': + dependencies: + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.864.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.864.0 - '@aws-sdk/credential-provider-http': 3.864.0 - '@aws-sdk/credential-provider-ini': 3.864.0 - '@aws-sdk/credential-provider-process': 3.864.0 - '@aws-sdk/credential-provider-sso': 3.864.0 - '@aws-sdk/credential-provider-web-identity': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.0.7 - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.4.0 + '@aws-sdk/credential-provider-node@3.925.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.922.0 + '@aws-sdk/credential-provider-http': 3.922.0 + '@aws-sdk/credential-provider-ini': 3.925.0 + '@aws-sdk/credential-provider-process': 3.922.0 + '@aws-sdk/credential-provider-sso': 3.925.0 + '@aws-sdk/credential-provider-web-identity': 3.925.0 + '@aws-sdk/types': 3.922.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.883.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.883.0 - '@aws-sdk/credential-provider-http': 3.883.0 - '@aws-sdk/credential-provider-ini': 3.883.0 - '@aws-sdk/credential-provider-process': 3.883.0 - '@aws-sdk/credential-provider-sso': 3.883.0 - '@aws-sdk/credential-provider-web-identity': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/credential-provider-imds': 4.1.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/credential-provider-node@3.971.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.970.0 + '@aws-sdk/credential-provider-http': 3.970.0 + '@aws-sdk/credential-provider-ini': 3.971.0 + '@aws-sdk/credential-provider-process': 3.970.0 + '@aws-sdk/credential-provider-sso': 3.971.0 + '@aws-sdk/credential-provider-web-identity': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.864.0': + '@aws-sdk/credential-provider-process@3.922.0': dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-process@3.883.0': + '@aws-sdk/credential-provider-process@3.970.0': dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.864.0': + '@aws-sdk/credential-provider-sso@3.925.0': dependencies: - '@aws-sdk/client-sso': 3.864.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/token-providers': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/client-sso': 3.925.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/token-providers': 3.925.0 + '@aws-sdk/types': 3.922.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-sso@3.883.0': + '@aws-sdk/credential-provider-sso@3.971.0': dependencies: - '@aws-sdk/client-sso': 3.883.0 - '@aws-sdk/core': 3.883.0 - '@aws-sdk/token-providers': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/client-sso': 3.971.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/token-providers': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.864.0': + '@aws-sdk/credential-provider-web-identity@3.925.0': dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/nested-clients': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/nested-clients': 3.925.0 + '@aws-sdk/types': 3.922.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.883.0': + '@aws-sdk/credential-provider-web-identity@3.971.0': dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/nested-clients': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/middleware-bucket-endpoint@3.873.0': + '@aws-sdk/middleware-bucket-endpoint@3.969.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-arn-parser': 3.873.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-config-provider': 4.1.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-arn-parser': 3.968.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-expect-continue@3.873.0': + '@aws-sdk/middleware-expect-continue@3.969.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.883.0': + '@aws-sdk/middleware-flexible-checksums@3.971.0': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/is-array-buffer': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-stream': 4.3.0 - '@smithy/util-utf8': 4.1.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/crc64-nvme': 3.969.0 + '@aws-sdk/types': 3.969.0 + '@smithy/is-array-buffer': 4.2.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.862.0': + '@aws-sdk/middleware-host-header@3.922.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.922.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.873.0': + '@aws-sdk/middleware-host-header@3.969.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-location-constraint@3.873.0': + '@aws-sdk/middleware-location-constraint@3.969.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.862.0': + '@aws-sdk/middleware-logger@3.922.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.922.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.876.0': + '@aws-sdk/middleware-logger@3.969.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.862.0': + '@aws-sdk/middleware-recursion-detection@3.922.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.922.0 + '@aws/lambda-invoke-store': 0.1.1 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.873.0': + '@aws-sdk/middleware-recursion-detection@3.969.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@aws/lambda-invoke-store': 0.2.2 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.864.0': - dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-arn-parser': 3.804.0 - '@smithy/core': 3.10.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/signature-v4': 5.1.3 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-config-provider': 4.1.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-stream': 4.3.0 - '@smithy/util-utf8': 4.1.0 + '@aws-sdk/middleware-sdk-s3@3.922.0': + dependencies: + '@aws-sdk/core': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@aws-sdk/util-arn-parser': 3.893.0 + '@smithy/core': 3.20.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.883.0': - dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-arn-parser': 3.873.0 - '@smithy/core': 3.10.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/signature-v4': 5.1.3 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-config-provider': 4.1.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-stream': 4.3.0 - '@smithy/util-utf8': 4.1.0 + '@aws-sdk/middleware-sdk-s3@3.970.0': + dependencies: + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-arn-parser': 3.968.0 + '@smithy/core': 3.20.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-ssec@3.873.0': + '@aws-sdk/middleware-ssec@3.971.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.864.0': + '@aws-sdk/middleware-user-agent@3.922.0': dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@smithy/core': 3.10.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@aws-sdk/util-endpoints': 3.922.0 + '@smithy/core': 3.20.6 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.883.0': + '@aws-sdk/middleware-user-agent@3.970.0': dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.879.0 - '@smithy/core': 3.10.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@smithy/core': 3.20.6 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.864.0': + '@aws-sdk/nested-clients@3.925.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.864.0 - '@aws-sdk/middleware-host-header': 3.862.0 - '@aws-sdk/middleware-logger': 3.862.0 - '@aws-sdk/middleware-recursion-detection': 3.862.0 - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/region-config-resolver': 3.862.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.862.0 - '@aws-sdk/util-user-agent-browser': 3.862.0 - '@aws-sdk/util-user-agent-node': 3.864.0 - '@smithy/config-resolver': 4.2.0 - '@smithy/core': 3.10.0 - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/middleware-retry': 4.2.0 - '@smithy/middleware-serde': 4.1.0 - '@smithy/middleware-stack': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.1.0 - '@smithy/util-defaults-mode-node': 4.1.0 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-retry': 4.1.0 - '@smithy/util-utf8': 4.1.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/middleware-host-header': 3.922.0 + '@aws-sdk/middleware-logger': 3.922.0 + '@aws-sdk/middleware-recursion-detection': 3.922.0 + '@aws-sdk/middleware-user-agent': 3.922.0 + '@aws-sdk/region-config-resolver': 3.925.0 + '@aws-sdk/types': 3.922.0 + '@aws-sdk/util-endpoints': 3.922.0 + '@aws-sdk/util-user-agent-browser': 3.922.0 + '@aws-sdk/util-user-agent-node': 3.922.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.6 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/middleware-retry': 4.4.23 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.22 + '@smithy/util-defaults-mode-node': 4.2.25 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/nested-clients@3.883.0': + '@aws-sdk/nested-clients@3.971.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.883.0 - '@aws-sdk/middleware-host-header': 3.873.0 - '@aws-sdk/middleware-logger': 3.876.0 - '@aws-sdk/middleware-recursion-detection': 3.873.0 - '@aws-sdk/middleware-user-agent': 3.883.0 - '@aws-sdk/region-config-resolver': 3.873.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-endpoints': 3.879.0 - '@aws-sdk/util-user-agent-browser': 3.873.0 - '@aws-sdk/util-user-agent-node': 3.883.0 - '@smithy/config-resolver': 4.2.0 - '@smithy/core': 3.10.0 - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/hash-node': 4.0.5 - '@smithy/invalid-dependency': 4.0.5 - '@smithy/middleware-content-length': 4.0.5 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/middleware-retry': 4.2.0 - '@smithy/middleware-serde': 4.1.0 - '@smithy/middleware-stack': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.1.0 - '@smithy/util-defaults-mode-node': 4.1.0 - '@smithy/util-endpoints': 3.0.7 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-retry': 4.1.0 - '@smithy/util-utf8': 4.1.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.6 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/middleware-retry': 4.4.23 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-body-length-node': 4.2.1 + '@smithy/util-defaults-mode-browser': 4.3.22 + '@smithy/util-defaults-mode-node': 4.2.25 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.862.0': + '@aws-sdk/region-config-resolver@3.925.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-config-provider': 4.0.0 - '@smithy/util-middleware': 4.1.0 + '@aws-sdk/types': 3.922.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/region-config-resolver@3.873.0': + '@aws-sdk/region-config-resolver@3.969.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-config-provider': 4.1.0 - '@smithy/util-middleware': 4.1.0 + '@aws-sdk/types': 3.969.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/s3-request-presigner@3.884.0': + '@aws-sdk/s3-request-presigner@3.971.0': dependencies: - '@aws-sdk/signature-v4-multi-region': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@aws-sdk/util-format-url': 3.873.0 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 + '@aws-sdk/signature-v4-multi-region': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-format-url': 3.969.0 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.864.0': + '@aws-sdk/signature-v4-multi-region@3.922.0': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/signature-v4': 5.1.3 - '@smithy/types': 4.4.0 + '@aws-sdk/middleware-sdk-s3': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.883.0': + '@aws-sdk/signature-v4-multi-region@3.970.0': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/signature-v4': 5.1.3 - '@smithy/types': 4.4.0 + '@aws-sdk/middleware-sdk-s3': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.864.0': + '@aws-sdk/token-providers@3.925.0': dependencies: - '@aws-sdk/core': 3.864.0 - '@aws-sdk/nested-clients': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.922.0 + '@aws-sdk/nested-clients': 3.925.0 + '@aws-sdk/types': 3.922.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/token-providers@3.883.0': + '@aws-sdk/token-providers@3.971.0': dependencies: - '@aws-sdk/core': 3.883.0 - '@aws-sdk/nested-clients': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.862.0': + '@aws-sdk/types@3.922.0': dependencies: - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.804.0': + '@aws-sdk/types@3.969.0': dependencies: + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.873.0': + '@aws-sdk/util-arn-parser@3.893.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.862.0': + '@aws-sdk/util-arn-parser@3.968.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-endpoints': 3.0.7 tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.879.0': + '@aws-sdk/util-endpoints@3.922.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-endpoints': 3.0.7 + '@aws-sdk/types': 3.922.0 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.873.0': + '@aws-sdk/util-endpoints@3.970.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/querystring-builder': 4.1.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.723.0': + '@aws-sdk/util-format-url@3.969.0': dependencies: + '@aws-sdk/types': 3.969.0 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.862.0': + '@aws-sdk/util-locate-window@3.893.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 - bowser: 2.11.0 tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.873.0': + '@aws-sdk/util-user-agent-browser@3.922.0': dependencies: - '@aws-sdk/types': 3.862.0 - '@smithy/types': 4.4.0 - bowser: 2.11.0 + '@aws-sdk/types': 3.922.0 + '@smithy/types': 4.12.0 + bowser: 2.12.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.864.0': + '@aws-sdk/util-user-agent-browser@3.969.0': dependencies: - '@aws-sdk/middleware-user-agent': 3.864.0 - '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 + bowser: 2.12.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.883.0': + '@aws-sdk/util-user-agent-node@3.922.0': dependencies: - '@aws-sdk/middleware-user-agent': 3.883.0 - '@aws-sdk/types': 3.862.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/types': 4.4.0 + '@aws-sdk/middleware-user-agent': 3.922.0 + '@aws-sdk/types': 3.922.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.862.0': + '@aws-sdk/util-user-agent-node@3.971.0': dependencies: - '@smithy/types': 4.4.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.921.0': + dependencies: + '@smithy/types': 4.12.0 + fast-xml-parser: 5.2.5 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.873.0': + '@aws-sdk/xml-builder@3.969.0': dependencies: - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 + fast-xml-parser: 5.2.5 tslib: 2.8.1 + '@aws/lambda-invoke-store@0.1.1': {} + + '@aws/lambda-invoke-store@0.2.2': {} + '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.0': {} + '@babel/compat-data@7.28.5': {} + + '@babel/compat-data@7.28.6': {} - '@babel/core@7.28.4': + '@babel/core@7.28.6': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) - '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@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/core@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.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.28.3': + '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.24.7': + '@babel/generator@7.28.6': dependencies: - '@babel/types': 7.28.2 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.28.0 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.1 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.24.8(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.8 - '@babel/helper-optimise-call-expression': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.28.4) - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.4)': + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.6) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.4)': + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.2.0 + regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.4)': + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) lodash.debounce: 4.0.8 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color - '@babel/helper-environment-visitor@7.24.7': - dependencies: - '@babel/types': 7.28.2 - - '@babel/helper-function-name@7.24.7': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.24.8': + '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-member-expression-to-functions@7.27.1': + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.27.1': + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.24.7': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': dependencies: - '@babel/types': 7.28.2 + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.2 - - '@babel/helper-plugin-utils@7.24.8': {} + '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.4)': + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.27.1 - '@babel/traverse': 7.28.3 - transitivePeerDependencies: - - supports-color - - '@babel/helper-replace-supers@7.24.7(@babel/core@7.28.4)': - dependencies: - '@babel/core': 7.28.4 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.8 - '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/helper-wrap-function': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/core': 7.28.6 + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.3 - transitivePeerDependencies: - - supports-color - - '@babel/helper-skip-transparent-expression-wrappers@7.24.7': - dependencies: - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-split-export-declaration@7.24.7': - dependencies: - '@babel/types': 7.28.2 - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} - '@babel/helper-wrap-function@7.27.1': + '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helpers@7.28.4': - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - - '@babel/parser@7.26.10': + '@babel/helpers@7.28.6': dependencies: - '@babel/types': 7.28.2 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 - '@babel/parser@7.28.3': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 - '@babel/parser@7.28.4': + '@babel/parser@7.28.6': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.6 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.28.4)': + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.24.8(@babel/core@7.28.4) - '@babel/helper-plugin-utils': 7.24.8 + '@babel/core': 7.28.6 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.6) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.4)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.4)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.4)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.4)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.4)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.4)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.4)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.4)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.3 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.6) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.3 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.6) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 '@babel/template': 7.27.2 - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-exponentiation-operator@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-logical-assignment-operators@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) - '@babel/traverse': 7.28.3 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.6) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.4)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.2 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.6) + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.28.4)': + '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-runtime@7.28.5(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.6) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.6) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.6) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.6) '@babel/helper-plugin-utils': 7.27.1 - '@babel/preset-env@7.28.0(@babel/core@7.28.4)': + '@babel/preset-env@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.4 + '@babel/compat-data': 7.28.5 + '@babel/core': 7.28.6 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.4) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.4) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-regenerator': 7.28.1(@babel/core@7.28.4) - '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.4) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.4) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.4) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.4) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.4) - core-js-compat: 3.45.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.28.6) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.6) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.6) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.6) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-block-scoping': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.6) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.6) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.6) + '@babel/plugin-transform-exponentiation-operator': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-logical-assignment-operators': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.28.6) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.28.6) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.6) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.6) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.6) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.6) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.6) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.6) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.6) + core-js-compat: 3.46.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.4)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 esutils: 2.0.3 - '@babel/preset-react@7.27.1(@babel/core@7.28.4)': + '@babel/preset-react@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.4) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.28.4)': + '@babel/preset-typescript@7.28.5(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - '@babel/runtime-corejs3@7.26.0': + '@babel/runtime-corejs3@7.28.4': dependencies: - core-js-pure: 3.39.0 - regenerator-runtime: 0.14.1 - - '@babel/runtime@7.27.1': {} + core-js-pure: 3.46.0 - '@babel/runtime@7.28.2': {} + '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 - '@babel/traverse@7.28.3': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - debug: 4.4.1(supports-color@10.1.0) + '@babel/types': 7.28.5 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.4': + '@babel/traverse@7.28.6': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 - debug: 4.4.1(supports-color@10.1.0) + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color - '@babel/types@7.28.2': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.28.4': + '@babel/types@7.28.6': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@bcoe/v8-coverage@0.2.3': {} + '@borewit/text-codec@0.1.1': {} + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -12575,16 +15189,21 @@ snapshots: '@css-inline/css-inline-linux-x64-musl': 0.14.1 '@css-inline/css-inline-win32-x64-msvc': 0.14.1 - '@csstools/color-helpers@5.0.2': {} + '@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/color-helpers': 5.0.2 + '@csstools/color-helpers': 5.1.0 '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 @@ -12595,58 +15214,1090 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.0)': + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-color-function@4.0.12(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.6)': + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.6)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-initial@2.0.1(postcss@8.5.6)': dependencies: + postcss: 8.5.6 + + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 postcss-selector-parser: 7.1.0 - '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.6)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.6)': + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-normalize-display-values@4.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-random-function@2.0.1(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.6)': + dependencies: + '@csstools/color-helpers': 5.1.0 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@csstools/utilities@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@discoveryjs/json-ext@0.5.7': {} + + '@dnd-kit/accessibility@3.1.1(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + + '@docsearch/css@3.9.0': {} + + '@docsearch/react@3.9.0(@algolia/client-search@5.43.0)(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.9(@algolia/client-search@5.43.0)(algoliasearch@5.43.0) + '@docsearch/css': 3.9.0 + algoliasearch: 5.43.0 + optionalDependencies: + '@types/react': 19.2.8 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@docusaurus/babel@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/core': 7.28.6 + '@babel/generator': 7.28.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-transform-runtime': 7.28.5(@babel/core@7.28.6) + '@babel/preset-env': 7.28.5(@babel/core@7.28.6) + '@babel/preset-react': 7.28.5(@babel/core@7.28.6) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.6) + '@babel/runtime': 7.28.4 + '@babel/runtime-corejs3': 7.28.4 + '@babel/traverse': 7.28.5 + '@docusaurus/logger': 3.9.2 + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + babel-plugin-dynamic-import-node: 2.3.3 + fs-extra: 11.3.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/bundler@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@babel/core': 7.28.6 + '@docusaurus/babel': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/cssnano-preset': 3.9.2 + '@docusaurus/logger': 3.9.2 + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + babel-loader: 9.2.1(@babel/core@7.28.6)(webpack@5.104.1) + clean-css: 5.3.3 + copy-webpack-plugin: 11.0.0(webpack@5.104.1) + css-loader: 6.11.0(webpack@5.104.1) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.104.1) + cssnano: 6.1.2(postcss@8.5.6) + file-loader: 6.2.0(webpack@5.104.1) + html-minifier-terser: 7.2.0 + mini-css-extract-plugin: 2.9.4(webpack@5.104.1) + null-loader: 4.0.1(webpack@5.104.1) + postcss: 8.5.6 + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1) + postcss-preset-env: 10.4.0(postcss@8.5.6) + terser-webpack-plugin: 5.3.14(webpack@5.104.1) + tslib: 2.8.1 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) + webpack: 5.104.1 + webpackbar: 6.0.1(webpack@5.104.1) + transitivePeerDependencies: + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - csso + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - typescript + - uglify-js + - webpack-cli + + '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/babel': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/bundler': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@19.2.3) + boxen: 6.2.1 + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.5 + combine-promises: 1.2.0 + commander: 5.1.0 + core-js: 3.46.0 + detect-port: 1.6.1 + escape-html: 1.0.3 + eta: 2.2.0 + eval: 0.1.8 + execa: 5.1.1 + fs-extra: 11.3.2 + html-tags: 3.3.1 + html-webpack-plugin: 5.6.4(webpack@5.100.2) + leven: 3.1.0 + lodash: 4.17.21 + open: 8.4.2 + p-map: 4.0.0 + prompts: 2.4.2 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.3)' + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.3))(webpack@5.100.2) + react-router: 5.3.4(react@19.2.3) + react-router-config: 5.1.1(react-router@5.3.4(react@19.2.3))(react@19.2.3) + react-router-dom: 5.3.4(react@19.2.3) + semver: 7.7.3 + serve-handler: 6.1.6 + tinypool: 1.1.1 + tslib: 2.8.1 + update-notifier: 6.0.2 + webpack: 5.100.2 + webpack-bundle-analyzer: 4.10.2 + webpack-dev-server: 5.2.2(webpack@5.100.2) + webpack-merge: 6.0.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/cssnano-preset@3.9.2': + dependencies: + cssnano-preset-advanced: 6.1.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-sort-media-queries: 5.2.0(postcss@8.5.6) + tslib: 2.8.1 + + '@docusaurus/logger@3.9.2': + dependencies: + chalk: 4.1.2 + tslib: 2.8.1 + + '@docusaurus/mdx-loader@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@docusaurus/logger': 3.9.2 + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@mdx-js/mdx': 3.1.1 + '@slorber/remark-comment': 1.0.0 + escape-html: 1.0.3 + estree-util-value-to-estree: 3.5.0 + file-loader: 6.2.0(webpack@5.104.1) + fs-extra: 11.3.2 + image-size: 2.0.2 + mdast-util-mdx: 3.0.0 + mdast-util-to-string: 4.0.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + rehype-raw: 7.0.0 + remark-directive: 3.0.1 + remark-emoji: 4.0.1 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.1 + stringify-object: 3.3.0 + tslib: 2.8.1 + unified: 11.0.5 + unist-util-visit: 5.0.0 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) + vfile: 6.0.3 + webpack: 5.104.1 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/module-type-aliases@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/history': 4.7.11 + '@types/react': 19.2.6 + '@types/react-router-config': 5.0.11 + '@types/react-router-dom': 5.3.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.3)' + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + cheerio: 1.0.0-rc.12 + feed: 4.2.2 + fs-extra: 11.3.2 + lodash: 4.17.21 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + schema-dts: 1.1.5 + srcset: 4.0.0 + tslib: 2.8.1 + unist-util-visit: 5.0.0 + utility-types: 3.11.0 + webpack: 5.104.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/react-router-config': 5.0.11 + combine-promises: 1.2.0 + fs-extra: 11.3.2 + js-yaml: 4.1.1 + lodash: 4.17.21 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + schema-dts: 1.1.5 + tslib: 2.8.1 + utility-types: 3.11.0 + webpack: 5.104.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + fs-extra: 11.3.2 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + webpack: 5.104.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + fs-extra: 11.3.2 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-json-view-lite: 2.5.0(react@19.2.3) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/gtag.js': 0.0.12 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + fs-extra: 11.3.2 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + sitemap: 7.1.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/webpack': 8.1.0(typescript@5.9.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + webpack: 5.104.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3) + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@algolia/client-search' + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/react-loadable@6.0.0(react@19.2.3)': + dependencies: + '@types/react': 19.2.8 + react: 19.2.3 + + '@docusaurus/theme-classic@3.9.2(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-translations': 3.9.2 + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@19.2.3) + clsx: 2.1.1 + infima: 0.2.0-alpha.45 + lodash: 4.17.21 + nprogress: 0.2.0 + postcss: 8.5.6 + prism-react-renderer: 2.4.1(react@19.2.3) + prismjs: 1.30.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-router-dom: 5.3.4(react@19.2.3) + rtlcss: 4.3.0 + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - postcss-selector-parser: 7.1.0 + '@docusaurus/mdx-loader': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/history': 4.7.11 + '@types/react': 19.2.8 + '@types/react-router-config': 5.0.11 + clsx: 2.1.1 + parse-numeric-range: 1.3.0 + prism-react-renderer: 2.4.1(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli - '@dnd-kit/accessibility@3.1.1(react@19.1.1)': - dependencies: - react: 19.1.1 + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.43.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3)(typescript@5.9.3)': + dependencies: + '@docsearch/react': 3.9.0(@algolia/client-search@5.43.0)(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(search-insights@2.17.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/theme-translations': 3.9.2 + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-validation': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + algoliasearch: 5.43.0 + algoliasearch-helper: 3.26.1(algoliasearch@5.43.0) + clsx: 2.1.1 + eta: 2.2.0 + fs-extra: 11.3.2 + lodash: 4.17.21 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@algolia/client-search' + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli - '@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@docusaurus/theme-translations@3.9.2': dependencies: - '@dnd-kit/accessibility': 3.1.1(react@19.1.1) - '@dnd-kit/utilities': 3.2.2(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + fs-extra: 11.3.2 tslib: 2.8.1 - '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)': + '@docusaurus/tsconfig@3.9.2': {} + + '@docusaurus/types@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@mdx-js/mdx': 3.1.1 + '@types/history': 4.7.11 + '@types/mdast': 4.0.4 + '@types/react': 19.2.6 + commander: 5.1.0 + joi: 17.13.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)' + utility-types: 3.11.0 + webpack: 5.100.2 + webpack-merge: 5.10.0 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/utils-common@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@dnd-kit/utilities': 3.2.2(react@19.1.1) - react: 19.1.1 + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)': + '@docusaurus/utils-validation@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@dnd-kit/utilities': 3.2.2(react@19.1.1) - react: 19.1.1 + '@docusaurus/logger': 3.9.2 + '@docusaurus/utils': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + fs-extra: 11.3.2 + joi: 17.13.3 + js-yaml: 4.1.1 + lodash: 4.17.21 tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli - '@dnd-kit/utilities@3.2.2(react@19.1.1)': + '@docusaurus/utils@3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - react: 19.1.1 - tslib: 2.6.3 + '@docusaurus/logger': 3.9.2 + '@docusaurus/types': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@docusaurus/utils-common': 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + escape-string-regexp: 4.0.0 + execa: 5.1.1 + file-loader: 6.2.0(webpack@5.104.1) + fs-extra: 11.3.2 + github-slugger: 1.5.0 + globby: 11.1.0 + gray-matter: 4.0.3 + jiti: 1.21.7 + js-yaml: 4.1.1 + lodash: 4.17.21 + micromatch: 4.0.8 + p-queue: 6.6.2 + prompts: 2.4.2 + resolve-pathname: 3.0.0 + tslib: 2.8.1 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) + utility-types: 3.11.0 + webpack: 5.104.1 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli - '@emnapi/core@1.4.5': + '@emnapi/core@1.7.0': dependencies: - '@emnapi/wasi-threads': 1.0.4 + '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.5': + '@emnapi/runtime@1.7.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.0.4': + '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 optional: true @@ -12654,7 +16305,7 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.27.1 - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -12679,19 +16330,19 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.1.12)(react@19.1.1)': + '@emotion/react@11.14.0(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.3) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 transitivePeerDependencies: - supports-color @@ -12701,149 +16352,155 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.3 '@emotion/sheet@1.4.0': {} '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.1)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 '@emotion/utils@1.4.2': {} '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.25.0': + '@esbuild/aix-ppc64@0.27.0': optional: true - '@esbuild/android-arm64@0.25.0': + '@esbuild/android-arm64@0.27.0': optional: true - '@esbuild/android-arm@0.25.0': + '@esbuild/android-arm@0.27.0': optional: true - '@esbuild/android-x64@0.25.0': + '@esbuild/android-x64@0.27.0': optional: true - '@esbuild/darwin-arm64@0.25.0': + '@esbuild/darwin-arm64@0.27.0': optional: true - '@esbuild/darwin-x64@0.25.0': + '@esbuild/darwin-x64@0.27.0': optional: true - '@esbuild/freebsd-arm64@0.25.0': + '@esbuild/freebsd-arm64@0.27.0': optional: true - '@esbuild/freebsd-x64@0.25.0': + '@esbuild/freebsd-x64@0.27.0': optional: true - '@esbuild/linux-arm64@0.25.0': + '@esbuild/linux-arm64@0.27.0': optional: true - '@esbuild/linux-arm@0.25.0': + '@esbuild/linux-arm@0.27.0': optional: true - '@esbuild/linux-ia32@0.25.0': + '@esbuild/linux-ia32@0.27.0': optional: true - '@esbuild/linux-loong64@0.25.0': + '@esbuild/linux-loong64@0.27.0': optional: true - '@esbuild/linux-mips64el@0.25.0': + '@esbuild/linux-mips64el@0.27.0': optional: true - '@esbuild/linux-ppc64@0.25.0': + '@esbuild/linux-ppc64@0.27.0': optional: true - '@esbuild/linux-riscv64@0.25.0': + '@esbuild/linux-riscv64@0.27.0': optional: true - '@esbuild/linux-s390x@0.25.0': + '@esbuild/linux-s390x@0.27.0': optional: true - '@esbuild/linux-x64@0.25.0': + '@esbuild/linux-x64@0.27.0': optional: true - '@esbuild/netbsd-arm64@0.25.0': + '@esbuild/netbsd-arm64@0.27.0': optional: true - '@esbuild/netbsd-x64@0.25.0': + '@esbuild/netbsd-x64@0.27.0': optional: true - '@esbuild/openbsd-arm64@0.25.0': + '@esbuild/openbsd-arm64@0.27.0': optional: true - '@esbuild/openbsd-x64@0.25.0': + '@esbuild/openbsd-x64@0.27.0': optional: true - '@esbuild/sunos-x64@0.25.0': + '@esbuild/openharmony-arm64@0.27.0': optional: true - '@esbuild/win32-arm64@0.25.0': + '@esbuild/sunos-x64@0.27.0': optional: true - '@esbuild/win32-ia32@0.25.0': + '@esbuild/win32-arm64@0.27.0': optional: true - '@esbuild/win32-x64@0.25.0': + '@esbuild/win32-ia32@0.27.0': optional: true - '@eslint-community/eslint-utils@4.8.0(eslint@9.35.0(jiti@1.21.7))': - dependencies: - eslint: 9.35.0(jiti@1.21.7) - eslint-visitor-keys: 3.4.3 + '@esbuild/win32-x64@0.27.0': + optional: true - '@eslint-community/eslint-utils@4.8.0(eslint@9.35.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@1.21.7))': dependencies: - eslint: 9.35.0(jiti@2.4.2) + eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.3.2(eslint@9.35.0(jiti@2.4.2))': + '@eslint/compat@2.0.1(eslint@9.39.2(jiti@1.21.7))': + dependencies: + '@eslint/core': 1.0.1 optionalDependencies: - eslint: 9.35.0(jiti@2.4.2) + eslint: 9.39.2(jiti@1.21.7) - '@eslint/config-array@0.21.0': + '@eslint/config-array@0.21.1': dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.1(supports-color@10.1.0) + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@10.2.2) minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.3.1': {} + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 - '@eslint/core@0.15.2': + '@eslint/core@1.0.1': dependencies: '@types/json-schema': 7.0.15 '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.35.0': {} + '@eslint/js@9.39.2': {} - '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.5': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.15.2 + '@eslint/core': 0.17.0 levn: 0.4.1 - '@faker-js/faker@9.9.0': {} + '@faker-js/faker@10.2.0': {} '@fast-csv/format@4.3.5': dependencies: @@ -12882,56 +16539,49 @@ snapshots: '@fastify/accept-negotiator@2.0.1': {} - '@fastify/ajv-compiler@4.0.2': + '@fastify/ajv-compiler@4.0.5': dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) - fast-uri: 3.0.6 + fast-uri: 3.1.0 - '@fastify/busboy@3.1.1': {} + '@fastify/busboy@3.2.0': {} - '@fastify/cors@11.1.0': + '@fastify/cors@11.2.0': dependencies: - fastify-plugin: 5.0.1 + fastify-plugin: 5.1.0 toad-cache: 3.7.0 '@fastify/deepmerge@3.1.0': {} - '@fastify/error@4.1.0': {} + '@fastify/error@4.2.0': {} - '@fastify/fast-json-stringify-compiler@5.0.2': + '@fastify/fast-json-stringify-compiler@5.0.3': dependencies: - fast-json-stringify: 6.0.1 + fast-json-stringify: 6.1.1 '@fastify/formbody@8.0.2': dependencies: fast-querystring: 1.1.2 - fastify-plugin: 5.0.1 + fastify-plugin: 5.1.0 - '@fastify/forwarded@3.0.0': {} + '@fastify/forwarded@3.0.1': {} '@fastify/merge-json-schemas@0.2.1': dependencies: dequal: 2.0.3 - '@fastify/middie@9.0.3': + '@fastify/multipart@9.4.0': dependencies: - '@fastify/error': 4.1.0 - fastify-plugin: 5.0.1 - path-to-regexp: 8.2.0 - reusify: 1.1.0 - - '@fastify/multipart@9.2.1': - dependencies: - '@fastify/busboy': 3.1.1 + '@fastify/busboy': 3.2.0 '@fastify/deepmerge': 3.1.0 - '@fastify/error': 4.1.0 - fastify-plugin: 5.0.1 - secure-json-parse: 4.0.0 + '@fastify/error': 4.2.0 + fastify-plugin: 5.1.0 + secure-json-parse: 4.1.0 - '@fastify/proxy-addr@5.0.0': + '@fastify/proxy-addr@5.1.0': dependencies: - '@fastify/forwarded': 3.0.0 + '@fastify/forwarded': 3.0.1 ipaddr.js: 2.2.0 '@fastify/send@4.1.0': @@ -12942,31 +16592,31 @@ snapshots: http-errors: 2.0.0 mime: 3.0.0 - '@fastify/static@8.2.0': + '@fastify/static@9.0.0': dependencies: '@fastify/accept-negotiator': 2.0.1 '@fastify/send': 4.1.0 - content-disposition: 0.5.4 - fastify-plugin: 5.0.1 + content-disposition: 1.0.1 + fastify-plugin: 5.1.0 fastq: 1.19.1 - glob: 11.0.3 + glob: 13.0.0 - '@floating-ui/core@1.7.0': + '@floating-ui/core@1.7.3': dependencies: - '@floating-ui/utils': 0.2.9 + '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.0': + '@floating-ui/dom@1.7.4': dependencies: - '@floating-ui/core': 1.7.0 - '@floating-ui/utils': 0.2.9 + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@floating-ui/dom': 1.7.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@floating-ui/dom': 1.7.4 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - '@floating-ui/utils@0.2.9': {} + '@floating-ui/utils@0.2.10': {} '@hapi/address@5.1.1': dependencies: @@ -12976,360 +16626,282 @@ snapshots: '@hapi/hoek@11.0.7': {} + '@hapi/hoek@9.3.0': {} + '@hapi/pinpoint@2.0.1': {} - '@hapi/tlds@1.1.2': {} + '@hapi/tlds@1.1.4': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 '@hapi/topo@6.0.2': dependencies: '@hapi/hoek': 11.0.7 - '@hookform/resolvers@5.2.1(react-hook-form@7.62.0(react@19.1.1))': + '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.2.3))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.62.0(react@19.1.1) + react-hook-form: 7.71.1(react@19.2.3) '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.7': dependencies: '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.1': {} - - '@humanwhocodes/retry@0.4.2': {} + '@humanwhocodes/retry@0.4.3': {} - '@ianvs/prettier-plugin-sort-imports@4.7.0(prettier@3.5.3)': + '@ianvs/prettier-plugin-sort-imports@4.7.0(prettier@3.6.2)': dependencies: - '@babel/generator': 7.28.3 - '@babel/parser': 7.28.3 - '@babel/traverse': 7.28.3 - '@babel/types': 7.28.2 - prettier: 3.5.3 - semver: 7.7.2 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + prettier: 3.6.2 + semver: 7.7.3 transitivePeerDependencies: - supports-color - '@img/sharp-darwin-arm64@0.34.3': + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.3': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.0': + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.34.3': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-linux-arm@0.34.3': + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.34.3': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.3': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.3': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.3': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.3': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.3': - dependencies: - '@emnapi/runtime': 1.4.5 + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-win32-arm64@0.34.3': + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-win32-ia32@0.34.3': + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.0 optional: true - '@img/sharp-win32-x64@0.34.3': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@inquirer/checkbox@4.1.5(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.18.1) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.18.1 + '@img/sharp-win32-ia32@0.34.5': + optional: true - '@inquirer/checkbox@4.2.0(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.18.1) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.18.1 + '@img/sharp-win32-x64@0.34.5': + optional: true - '@inquirer/confirm@5.1.14(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/type': 3.0.8(@types/node@22.18.1) - optionalDependencies: - '@types/node': 22.18.1 + '@inquirer/ansi@1.0.2': {} - '@inquirer/confirm@5.1.9(@types/node@22.18.1)': + '@inquirer/checkbox@4.3.2(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/type': 3.0.6(@types/node@22.18.1) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.8) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/core@10.1.10(@types/node@22.18.1)': + '@inquirer/confirm@5.1.21(@types/node@24.10.8)': dependencies: - '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.18.1) - ansi-escapes: 4.3.2 - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/type': 3.0.10(@types/node@24.10.8) optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/core@10.1.15(@types/node@22.18.1)': + '@inquirer/core@10.3.2(@types/node@24.10.8)': dependencies: - '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.18.1) - ansi-escapes: 4.3.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.8) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/editor@4.2.10(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/type': 3.0.6(@types/node@22.18.1) - external-editor: 3.1.0 - optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/editor@4.2.15(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/type': 3.0.8(@types/node@22.18.1) - external-editor: 3.1.0 - optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/expand@4.0.12(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/type': 3.0.6(@types/node@22.18.1) - yoctocolors-cjs: 2.1.2 - optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/expand@4.0.17(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/type': 3.0.8(@types/node@22.18.1) - yoctocolors-cjs: 2.1.2 + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/figures@1.0.11': {} - - '@inquirer/figures@1.0.13': {} + '@types/node': 24.10.8 - '@inquirer/input@4.1.9(@types/node@22.18.1)': + '@inquirer/editor@4.2.23(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/type': 3.0.6(@types/node@22.18.1) + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.8) + '@inquirer/type': 3.0.10(@types/node@24.10.8) optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/input@4.2.1(@types/node@22.18.1)': + '@inquirer/expand@4.0.23(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/type': 3.0.8(@types/node@22.18.1) + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/type': 3.0.10(@types/node@24.10.8) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/number@3.0.12(@types/node@22.18.1)': + '@inquirer/external-editor@1.0.3(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/type': 3.0.6(@types/node@22.18.1) + chardet: 2.1.1 + iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/number@3.0.17(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/type': 3.0.8(@types/node@22.18.1) - optionalDependencies: - '@types/node': 22.18.1 + '@inquirer/figures@1.0.15': {} - '@inquirer/password@4.0.12(@types/node@22.18.1)': + '@inquirer/input@4.3.1(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/type': 3.0.6(@types/node@22.18.1) - ansi-escapes: 4.3.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/type': 3.0.10(@types/node@24.10.8) optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/password@4.0.17(@types/node@22.18.1)': + '@inquirer/number@3.0.23(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/type': 3.0.8(@types/node@22.18.1) - ansi-escapes: 4.3.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/type': 3.0.10(@types/node@24.10.8) optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/prompts@7.3.2(@types/node@22.18.1)': - dependencies: - '@inquirer/checkbox': 4.1.5(@types/node@22.18.1) - '@inquirer/confirm': 5.1.9(@types/node@22.18.1) - '@inquirer/editor': 4.2.10(@types/node@22.18.1) - '@inquirer/expand': 4.0.12(@types/node@22.18.1) - '@inquirer/input': 4.1.9(@types/node@22.18.1) - '@inquirer/number': 3.0.12(@types/node@22.18.1) - '@inquirer/password': 4.0.12(@types/node@22.18.1) - '@inquirer/rawlist': 4.0.12(@types/node@22.18.1) - '@inquirer/search': 3.0.12(@types/node@22.18.1) - '@inquirer/select': 4.1.1(@types/node@22.18.1) - optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/prompts@7.8.0(@types/node@22.18.1)': - dependencies: - '@inquirer/checkbox': 4.2.0(@types/node@22.18.1) - '@inquirer/confirm': 5.1.14(@types/node@22.18.1) - '@inquirer/editor': 4.2.15(@types/node@22.18.1) - '@inquirer/expand': 4.0.17(@types/node@22.18.1) - '@inquirer/input': 4.2.1(@types/node@22.18.1) - '@inquirer/number': 3.0.17(@types/node@22.18.1) - '@inquirer/password': 4.0.17(@types/node@22.18.1) - '@inquirer/rawlist': 4.1.5(@types/node@22.18.1) - '@inquirer/search': 3.1.0(@types/node@22.18.1) - '@inquirer/select': 4.3.1(@types/node@22.18.1) - optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/rawlist@4.0.12(@types/node@22.18.1)': + '@inquirer/password@4.0.23(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/type': 3.0.6(@types/node@22.18.1) - yoctocolors-cjs: 2.1.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/type': 3.0.10(@types/node@24.10.8) optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/rawlist@4.1.5(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/type': 3.0.8(@types/node@22.18.1) - yoctocolors-cjs: 2.1.2 + '@types/node': 24.10.8 + + '@inquirer/prompts@7.10.1(@types/node@24.10.8)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.10.8) + '@inquirer/confirm': 5.1.21(@types/node@24.10.8) + '@inquirer/editor': 4.2.23(@types/node@24.10.8) + '@inquirer/expand': 4.0.23(@types/node@24.10.8) + '@inquirer/input': 4.3.1(@types/node@24.10.8) + '@inquirer/number': 3.0.23(@types/node@24.10.8) + '@inquirer/password': 4.0.23(@types/node@24.10.8) + '@inquirer/rawlist': 4.1.11(@types/node@24.10.8) + '@inquirer/search': 3.2.2(@types/node@24.10.8) + '@inquirer/select': 4.4.2(@types/node@24.10.8) optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/search@3.0.12(@types/node@22.18.1)': - dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.18.1) - yoctocolors-cjs: 2.1.2 + '@types/node': 24.10.8 + + '@inquirer/prompts@7.3.2(@types/node@24.10.8)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.10.8) + '@inquirer/confirm': 5.1.21(@types/node@24.10.8) + '@inquirer/editor': 4.2.23(@types/node@24.10.8) + '@inquirer/expand': 4.0.23(@types/node@24.10.8) + '@inquirer/input': 4.3.1(@types/node@24.10.8) + '@inquirer/number': 3.0.23(@types/node@24.10.8) + '@inquirer/password': 4.0.23(@types/node@24.10.8) + '@inquirer/rawlist': 4.1.11(@types/node@24.10.8) + '@inquirer/search': 3.2.2(@types/node@24.10.8) + '@inquirer/select': 4.4.2(@types/node@24.10.8) optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/search@3.1.0(@types/node@22.18.1)': + '@inquirer/rawlist@4.1.11(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.18.1) - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/type': 3.0.10(@types/node@24.10.8) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/select@4.1.1(@types/node@22.18.1)': + '@inquirer/search@3.2.2(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.18.1) - '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.18.1) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.8) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/select@4.3.1(@types/node@22.18.1)': + '@inquirer/select@4.4.2(@types/node@24.10.8)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.18.1) - '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.18.1) - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.8) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.8) + yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@inquirer/type@3.0.6(@types/node@22.18.1)': + '@inquirer/type@3.0.10(@types/node@24.10.8)': optionalDependencies: - '@types/node': 22.18.1 - - '@inquirer/type@3.0.8(@types/node@22.18.1)': - optionalDependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 '@isaacs/balanced-match@4.0.1': {} @@ -13341,7 +16913,7 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -13356,44 +16928,44 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@jest/console@30.1.2': + '@jest/console@30.2.0': dependencies: - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 chalk: 4.1.2 - jest-message-util: 30.1.0 - jest-util: 30.0.5 + jest-message-util: 30.2.0 + jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))': + '@jest/core@30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))': dependencies: - '@jest/console': 30.1.2 + '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 - '@jest/reporters': 30.1.3 - '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 4.3.0 + ci-info: 4.3.1 exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-changed-files: 30.0.5 - jest-config: 30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) - jest-haste-map: 30.1.0 - jest-message-util: 30.1.0 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 jest-regex-util: 30.0.1 - jest-resolve: 30.1.3 - jest-resolve-dependencies: 30.1.3 - jest-runner: 30.1.3 - jest-runtime: 30.1.3 - jest-snapshot: 30.1.2 - jest-util: 30.0.5 - jest-validate: 30.1.0 - jest-watcher: 30.1.3 + 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.0.5 + pretty-format: 30.2.0 slash: 3.0.0 transitivePeerDependencies: - babel-plugin-macros @@ -13401,144 +16973,142 @@ snapshots: - supports-color - ts-node - '@jest/create-cache-key-function@30.0.5': + '@jest/create-cache-key-function@30.2.0': dependencies: - '@jest/types': 30.0.5 + '@jest/types': 30.2.0 '@jest/diff-sequences@30.0.1': {} - '@jest/environment-jsdom-abstract@30.1.2(jsdom@26.1.0)': + '@jest/environment-jsdom-abstract@30.2.0(jsdom@26.1.0)': dependencies: - '@jest/environment': 30.1.2 - '@jest/fake-timers': 30.1.2 - '@jest/types': 30.0.5 + '@jest/environment': 30.2.0 + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 '@types/jsdom': 21.1.7 - '@types/node': 22.18.1 - jest-mock: 30.0.5 - jest-util: 30.0.5 + '@types/node': 24.10.8 + jest-mock: 30.2.0 + jest-util: 30.2.0 jsdom: 26.1.0 - '@jest/environment@30.1.2': - dependencies: - '@jest/fake-timers': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 22.18.1 - jest-mock: 30.0.5 - - '@jest/expect-utils@30.0.5': + '@jest/environment@30.2.0': dependencies: - '@jest/get-type': 30.0.1 + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 + jest-mock: 30.2.0 - '@jest/expect-utils@30.1.2': + '@jest/expect-utils@30.2.0': dependencies: '@jest/get-type': 30.1.0 - '@jest/expect@30.1.2': + '@jest/expect@30.2.0': dependencies: - expect: 30.1.2 - jest-snapshot: 30.1.2 + expect: 30.2.0 + jest-snapshot: 30.2.0 transitivePeerDependencies: - supports-color - '@jest/fake-timers@30.1.2': + '@jest/fake-timers@30.2.0': dependencies: - '@jest/types': 30.0.5 + '@jest/types': 30.2.0 '@sinonjs/fake-timers': 13.0.5 - '@types/node': 22.18.1 - jest-message-util: 30.1.0 - jest-mock: 30.0.5 - jest-util: 30.0.5 - - '@jest/get-type@30.0.1': {} + '@types/node': 24.10.8 + jest-message-util: 30.2.0 + jest-mock: 30.2.0 + jest-util: 30.2.0 '@jest/get-type@30.1.0': {} - '@jest/globals@30.1.2': + '@jest/globals@30.2.0': dependencies: - '@jest/environment': 30.1.2 - '@jest/expect': 30.1.2 - '@jest/types': 30.0.5 - jest-mock: 30.0.5 + '@jest/environment': 30.2.0 + '@jest/expect': 30.2.0 + '@jest/types': 30.2.0 + jest-mock: 30.2.0 transitivePeerDependencies: - supports-color '@jest/pattern@30.0.1': dependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 jest-regex-util: 30.0.1 - '@jest/reporters@30.1.3': + '@jest/reporters@30.2.0': dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 30.1.2 - '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@jridgewell/trace-mapping': 0.3.29 - '@types/node': 22.18.1 + '@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.31 + '@types/node': 24.10.8 chalk: 4.1.2 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 exit-x: 0.2.2 - glob: 10.4.5 + glob: 10.5.0 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - jest-message-util: 30.1.0 - jest-util: 30.0.5 - jest-worker: 30.1.0 + istanbul-reports: 3.2.0 + 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.3.0 transitivePeerDependencies: - supports-color + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + '@jest/schemas@30.0.5': dependencies: - '@sinclair/typebox': 0.34.38 + '@sinclair/typebox': 0.34.41 - '@jest/snapshot-utils@30.1.2': + '@jest/snapshot-utils@30.2.0': dependencies: - '@jest/types': 30.0.5 + '@jest/types': 30.2.0 chalk: 4.1.2 graceful-fs: 4.2.11 natural-compare: 1.4.0 '@jest/source-map@30.0.1': dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 - '@jest/test-result@30.1.3': + '@jest/test-result@30.2.0': dependencies: - '@jest/console': 30.1.2 - '@jest/types': 30.0.5 + '@jest/console': 30.2.0 + '@jest/types': 30.2.0 '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 - '@jest/test-sequencer@30.1.3': + '@jest/test-sequencer@30.2.0': dependencies: - '@jest/test-result': 30.1.3 + '@jest/test-result': 30.2.0 graceful-fs: 4.2.11 - jest-haste-map: 30.1.0 + jest-haste-map: 30.2.0 slash: 3.0.0 - '@jest/transform@30.1.2': + '@jest/transform@30.2.0': dependencies: - '@babel/core': 7.28.4 - '@jest/types': 30.0.5 - '@jridgewell/trace-mapping': 0.3.29 - babel-plugin-istanbul: 7.0.0 + '@babel/core': 7.28.6 + '@jest/types': 30.2.0 + '@jridgewell/trace-mapping': 0.3.31 + 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.1.0 + jest-haste-map: 30.2.0 jest-regex-util: 30.0.1 - jest-util: 30.0.5 + jest-util: 30.2.0 micromatch: 4.0.8 pirates: 4.0.7 slash: 3.0.0 @@ -13546,381 +17116,456 @@ snapshots: transitivePeerDependencies: - supports-color - '@jest/types@30.0.5': + '@jest/types@29.6.3': dependencies: - '@jest/pattern': 30.0.1 - '@jest/schemas': 30.0.5 + '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.18.1 - '@types/yargs': 17.0.33 + '@types/node': 24.10.8 + '@types/yargs': 17.0.34 chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.12': + '@jest/types@30.2.0': dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.29 + '@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': 24.10.8 + '@types/yargs': 17.0.34 + chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.29': + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.9': + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + tslib: 2.8.1 + + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.5.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + + '@leichtgewicht/ip-codec@2.0.5': {} '@lukeed/csprng@1.1.0': {} '@lukeed/ms@2.0.2': {} - '@microsoft/tsdoc@0.15.1': {} + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + acorn: 8.15.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.15.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@mdx-js/react@3.1.1(@types/react@19.2.8)(react@19.2.3)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.2.8 + react: 19.2.3 + + '@microsoft/tsdoc@0.16.0': {} + + '@napi-rs/nice-android-arm-eabi@1.1.1': + optional: true - '@napi-rs/nice-android-arm-eabi@1.0.1': + '@napi-rs/nice-android-arm64@1.1.1': optional: true - '@napi-rs/nice-android-arm64@1.0.1': + '@napi-rs/nice-darwin-arm64@1.1.1': optional: true - '@napi-rs/nice-darwin-arm64@1.0.1': + '@napi-rs/nice-darwin-x64@1.1.1': optional: true - '@napi-rs/nice-darwin-x64@1.0.1': + '@napi-rs/nice-freebsd-x64@1.1.1': optional: true - '@napi-rs/nice-freebsd-x64@1.0.1': + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': optional: true - '@napi-rs/nice-linux-arm-gnueabihf@1.0.1': + '@napi-rs/nice-linux-arm64-gnu@1.1.1': optional: true - '@napi-rs/nice-linux-arm64-gnu@1.0.1': + '@napi-rs/nice-linux-arm64-musl@1.1.1': optional: true - '@napi-rs/nice-linux-arm64-musl@1.0.1': + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': optional: true - '@napi-rs/nice-linux-ppc64-gnu@1.0.1': + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': optional: true - '@napi-rs/nice-linux-riscv64-gnu@1.0.1': + '@napi-rs/nice-linux-s390x-gnu@1.1.1': optional: true - '@napi-rs/nice-linux-s390x-gnu@1.0.1': + '@napi-rs/nice-linux-x64-gnu@1.1.1': optional: true - '@napi-rs/nice-linux-x64-gnu@1.0.1': + '@napi-rs/nice-linux-x64-musl@1.1.1': optional: true - '@napi-rs/nice-linux-x64-musl@1.0.1': + '@napi-rs/nice-openharmony-arm64@1.1.1': optional: true - '@napi-rs/nice-win32-arm64-msvc@1.0.1': + '@napi-rs/nice-win32-arm64-msvc@1.1.1': optional: true - '@napi-rs/nice-win32-ia32-msvc@1.0.1': + '@napi-rs/nice-win32-ia32-msvc@1.1.1': optional: true - '@napi-rs/nice-win32-x64-msvc@1.0.1': + '@napi-rs/nice-win32-x64-msvc@1.1.1': optional: true - '@napi-rs/nice@1.0.1': + '@napi-rs/nice@1.1.1': optionalDependencies: - '@napi-rs/nice-android-arm-eabi': 1.0.1 - '@napi-rs/nice-android-arm64': 1.0.1 - '@napi-rs/nice-darwin-arm64': 1.0.1 - '@napi-rs/nice-darwin-x64': 1.0.1 - '@napi-rs/nice-freebsd-x64': 1.0.1 - '@napi-rs/nice-linux-arm-gnueabihf': 1.0.1 - '@napi-rs/nice-linux-arm64-gnu': 1.0.1 - '@napi-rs/nice-linux-arm64-musl': 1.0.1 - '@napi-rs/nice-linux-ppc64-gnu': 1.0.1 - '@napi-rs/nice-linux-riscv64-gnu': 1.0.1 - '@napi-rs/nice-linux-s390x-gnu': 1.0.1 - '@napi-rs/nice-linux-x64-gnu': 1.0.1 - '@napi-rs/nice-linux-x64-musl': 1.0.1 - '@napi-rs/nice-win32-arm64-msvc': 1.0.1 - '@napi-rs/nice-win32-ia32-msvc': 1.0.1 - '@napi-rs/nice-win32-x64-msvc': 1.0.1 + '@napi-rs/nice-android-arm-eabi': 1.1.1 + '@napi-rs/nice-android-arm64': 1.1.1 + '@napi-rs/nice-darwin-arm64': 1.1.1 + '@napi-rs/nice-darwin-x64': 1.1.1 + '@napi-rs/nice-freebsd-x64': 1.1.1 + '@napi-rs/nice-linux-arm-gnueabihf': 1.1.1 + '@napi-rs/nice-linux-arm64-gnu': 1.1.1 + '@napi-rs/nice-linux-arm64-musl': 1.1.1 + '@napi-rs/nice-linux-ppc64-gnu': 1.1.1 + '@napi-rs/nice-linux-riscv64-gnu': 1.1.1 + '@napi-rs/nice-linux-s390x-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-musl': 1.1.1 + '@napi-rs/nice-openharmony-arm64': 1.1.1 + '@napi-rs/nice-win32-arm64-msvc': 1.1.1 + '@napi-rs/nice-win32-ia32-msvc': 1.1.1 + '@napi-rs/nice-win32-x64-msvc': 1.1.1 optional: true '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.4.5 - '@tybys/wasm-util': 0.10.0 + '@emnapi/core': 1.7.0 + '@emnapi/runtime': 1.7.0 + '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.0.3': + '@napi-rs/wasm-runtime@1.0.7': dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.4.5 - '@tybys/wasm-util': 0.10.0 + '@emnapi/core': 1.7.0 + '@emnapi/runtime': 1.7.0 + '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(nodemailer@7.0.6)': + '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(nodemailer@7.0.12)': dependencies: '@css-inline/css-inline': 0.14.1 - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) glob: 10.3.12 - nodemailer: 7.0.6 + nodemailer: 7.0.12 optionalDependencies: '@types/ejs': 3.1.5 '@types/mjml': 4.7.4 '@types/pug': 2.0.10 ejs: 3.1.10 handlebars: 4.7.8 - liquidjs: 10.15.0 - mjml: 4.15.3 - preview-email: 3.0.20 + liquidjs: 10.24.0 + mjml: 4.16.1 + preview-email: 3.1.0 pug: 3.0.3 transitivePeerDependencies: - encoding - '@nestjs/axios@4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.11.0)(rxjs@7.8.2)': + '@nestjs/axios@4.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - axios: 1.11.0 + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.13.2 rxjs: 7.8.2 - '@nestjs/cli@11.0.10(@swc/cli@0.7.8(@swc/core@1.13.5(@swc/helpers@0.5.17))(chokidar@4.0.3))(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)': + '@nestjs/cli@11.0.16(@swc/cli@0.7.10(@swc/core@1.13.5(@swc/helpers@0.5.18))(chokidar@4.0.3))(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)': dependencies: - '@angular-devkit/core': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.15(@types/node@22.18.1)(chokidar@4.0.3) - '@inquirer/prompts': 7.8.0(@types/node@22.18.1) - '@nestjs/schematics': 11.0.7(chokidar@4.0.3)(typescript@5.8.3) - ansis: 4.1.0 + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.19(@types/node@24.10.8)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@24.10.8) + '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))) - glob: 11.0.3 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18))) + glob: 13.0.0 node-emoji: 1.11.0 ora: 5.4.1 - tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.8.3 - webpack: 5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)) + typescript: 5.9.3 + webpack: 5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18)) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/cli': 0.7.8(@swc/core@1.13.5(@swc/helpers@0.5.17))(chokidar@4.0.3) - '@swc/core': 1.13.5(@swc/helpers@0.5.17) + '@swc/cli': 0.7.10(@swc/core@1.13.5(@swc/helpers@0.5.18))(chokidar@4.0.3) + '@swc/core': 1.13.5(@swc/helpers@0.5.18) transitivePeerDependencies: - '@types/node' - esbuild - uglify-js - webpack-cli - '@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.0.0 + file-type: 21.3.0 iterare: 1.2.1 - load-esm: 1.0.2 + load-esm: 1.0.3 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 optionalDependencies: class-transformer: 0.5.1 - class-validator: 0.14.2 + class-validator: 0.14.3 transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) dotenv: 16.4.7 dotenv-expand: 12.0.1 lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) - '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)': + '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) eventemitter2: 6.4.9 - '@nestjs/jwt@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@types/jsonwebtoken': 9.0.7 - jsonwebtoken: 9.0.2 + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 - class-validator: 0.14.2 + class-validator: 0.14.3 - '@nestjs/passport@11.0.5(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-express@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)': + '@nestjs/platform-express@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 - express: 5.1.0 + express: 5.2.1 multer: 2.0.2 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/platform-fastify@11.1.6(@fastify/static@8.2.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)': + '@nestjs/platform-fastify@11.1.12(@fastify/static@9.0.0)(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@fastify/cors': 11.1.0 + '@fastify/cors': 11.2.0 '@fastify/formbody': 8.0.2 - '@fastify/middie': 9.0.3 - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-querystring: 1.1.2 - fastify: 5.4.0 + fastify: 5.6.2 + fastify-plugin: 5.1.0 + find-my-way: 9.4.0 light-my-request: 6.6.0 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 + reusify: 1.1.0 tslib: 2.8.1 optionalDependencies: - '@fastify/static': 8.2.0 + '@fastify/static': 9.0.0 - '@nestjs/schedule@6.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)': + '@nestjs/schedule@6.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cron: 4.3.0 + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cron: 4.3.3 - '@nestjs/schematics@11.0.7(chokidar@4.0.3)(typescript@5.8.3)': + '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': dependencies: - '@angular-devkit/core': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - comment-json: 4.2.5 + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) + comment-json: 4.4.1 jsonc-parser: 3.3.1 pluralize: 8.0.0 - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.0(@fastify/static@8.2.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.5(@fastify/static@9.0.0)(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@microsoft/tsdoc': 0.15.1 - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) - js-yaml: 4.1.0 + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + js-yaml: 4.1.1 lodash: 4.17.21 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 reflect-metadata: 0.2.2 - swagger-ui-dist: 5.21.0 + swagger-ui-dist: 5.31.0 optionalDependencies: - '@fastify/static': 8.2.0 + '@fastify/static': 9.0.0 class-transformer: 0.5.1 - class-validator: 0.14.2 + class-validator: 0.14.3 - '@nestjs/terminus@11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.11.0)(rxjs@7.8.2))(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)))': + '@nestjs/terminus@11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)))': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 optionalDependencies: - '@nestjs/axios': 4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.11.0)(rxjs@7.8.2) - '@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))) - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) + '@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))) + typeorm: 0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) - '@nestjs/testing@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6)': + '@nestjs/testing@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)))': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + typeorm: 0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) - '@next/env@15.4.7': {} + '@next/env@16.1.3': {} - '@next/eslint-plugin-next@15.4.7': + '@next/eslint-plugin-next@16.1.3': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.4.7': + '@next/swc-darwin-arm64@16.1.3': optional: true - '@next/swc-darwin-x64@15.4.7': + '@next/swc-darwin-x64@16.1.3': optional: true - '@next/swc-linux-arm64-gnu@15.4.7': + '@next/swc-linux-arm64-gnu@16.1.3': optional: true - '@next/swc-linux-arm64-musl@15.4.7': + '@next/swc-linux-arm64-musl@16.1.3': optional: true - '@next/swc-linux-x64-gnu@15.4.7': + '@next/swc-linux-x64-gnu@16.1.3': optional: true - '@next/swc-linux-x64-musl@15.4.7': + '@next/swc-linux-x64-musl@16.1.3': optional: true - '@next/swc-win32-arm64-msvc@15.4.7': + '@next/swc-win32-arm64-msvc@16.1.3': optional: true - '@next/swc-win32-x64-msvc@15.4.7': + '@next/swc-win32-x64-msvc@16.1.3': optional: true '@noble/hashes@1.8.0': {} @@ -13998,12 +17643,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@number-flow/react@0.5.10(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@number-flow/react@0.5.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: esm-env: 1.2.2 number-flow: 0.5.8 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) '@nuxt/opencollective@0.4.1': dependencies: @@ -14015,7 +17660,7 @@ snapshots: '@opensearch-project/opensearch@3.5.1': dependencies: aws4: 1.13.2 - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) hpagent: 1.2.0 json11: 2.0.2 ms: 2.1.3 @@ -14025,662 +17670,678 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@oxc-resolver/binding-android-arm-eabi@11.6.1': + '@oxc-resolver/binding-android-arm-eabi@11.13.1': optional: true - '@oxc-resolver/binding-android-arm64@11.6.1': + '@oxc-resolver/binding-android-arm64@11.13.1': optional: true - '@oxc-resolver/binding-darwin-arm64@11.6.1': + '@oxc-resolver/binding-darwin-arm64@11.13.1': optional: true - '@oxc-resolver/binding-darwin-x64@11.6.1': + '@oxc-resolver/binding-darwin-x64@11.13.1': optional: true - '@oxc-resolver/binding-freebsd-x64@11.6.1': + '@oxc-resolver/binding-freebsd-x64@11.13.1': optional: true - '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.1': + '@oxc-resolver/binding-linux-arm-gnueabihf@11.13.1': optional: true - '@oxc-resolver/binding-linux-arm-musleabihf@11.6.1': + '@oxc-resolver/binding-linux-arm-musleabihf@11.13.1': optional: true - '@oxc-resolver/binding-linux-arm64-gnu@11.6.1': + '@oxc-resolver/binding-linux-arm64-gnu@11.13.1': optional: true - '@oxc-resolver/binding-linux-arm64-musl@11.6.1': + '@oxc-resolver/binding-linux-arm64-musl@11.13.1': optional: true - '@oxc-resolver/binding-linux-ppc64-gnu@11.6.1': + '@oxc-resolver/binding-linux-ppc64-gnu@11.13.1': optional: true - '@oxc-resolver/binding-linux-riscv64-gnu@11.6.1': + '@oxc-resolver/binding-linux-riscv64-gnu@11.13.1': optional: true - '@oxc-resolver/binding-linux-riscv64-musl@11.6.1': + '@oxc-resolver/binding-linux-riscv64-musl@11.13.1': optional: true - '@oxc-resolver/binding-linux-s390x-gnu@11.6.1': + '@oxc-resolver/binding-linux-s390x-gnu@11.13.1': optional: true - '@oxc-resolver/binding-linux-x64-gnu@11.6.1': + '@oxc-resolver/binding-linux-x64-gnu@11.13.1': optional: true - '@oxc-resolver/binding-linux-x64-musl@11.6.1': + '@oxc-resolver/binding-linux-x64-musl@11.13.1': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.6.1': + '@oxc-resolver/binding-wasm32-wasi@11.13.1': dependencies: - '@napi-rs/wasm-runtime': 1.0.3 + '@napi-rs/wasm-runtime': 1.0.7 optional: true - '@oxc-resolver/binding-win32-arm64-msvc@11.6.1': + '@oxc-resolver/binding-win32-arm64-msvc@11.13.1': optional: true - '@oxc-resolver/binding-win32-ia32-msvc@11.6.1': + '@oxc-resolver/binding-win32-ia32-msvc@11.13.1': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.6.1': + '@oxc-resolver/binding-win32-x64-msvc@11.13.1': optional: true - '@paralleldrive/cuid2@2.2.2': + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.2.9': {} - '@playwright/test@1.55.0': + '@playwright/test@1.57.0': + dependencies: + playwright: 1.57.0 + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': dependencies: - playwright: 1.55.0 + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@polka/url@1.0.0-next.29': {} '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.8)(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-context@1.1.2(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.8)(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - aria-hidden: 1.2.4 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-remove-scroll: 2.6.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.1(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-direction@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.8)(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-id@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - aria-hidden: 1.2.4 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-remove-scroll: 2.6.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.1(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - aria-hidden: 1.2.4 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-remove-scroll: 2.6.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.1(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) - - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': - dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.12)(react@19.1.1) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) '@radix-ui/rect': 1.1.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-primitive@2.1.2(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-slot': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-select@2.2.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - aria-hidden: 1.2.4 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-remove-scroll: 2.6.3(@types/react@19.1.12)(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.1(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-slot@1.2.2(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-slot@1.2.3(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-slot@1.2.4(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-toast@1.2.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-direction': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-use-size@1.1.1(@types/react@19.1.12)(react@19.1.1)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.8)(react@19.2.3)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.12)(react@19.1.1) - react: 19.1.1 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.8)(react@19.2.3) + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) '@radix-ui/rect@1.1.1': {} - '@redocly/ajv@8.11.2': + '@redocly/ajv@8.11.4': dependencies: fast-deep-equal: 3.1.3 json-schema-traverse: 1.0.0 @@ -14689,637 +18350,487 @@ snapshots: '@redocly/config@0.22.2': {} - '@redocly/openapi-core@1.34.5(supports-color@10.1.0)': + '@redocly/openapi-core@1.34.5(supports-color@10.2.2)': dependencies: - '@redocly/ajv': 8.11.2 + '@redocly/ajv': 8.11.4 '@redocly/config': 0.22.2 colorette: 1.4.0 - https-proxy-agent: 7.0.6(supports-color@10.1.0) + https-proxy-agent: 7.0.6(supports-color@10.2.2) js-levenshtein: 1.1.6 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 5.1.6 pluralize: 8.0.0 yaml-ast-parser: 0.0.43 transitivePeerDependencies: - supports-color - '@remixicon/react@4.6.0(react@19.1.1)': + '@reduxjs/toolkit@2.10.1(react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': dependencies: - react: 19.1.1 + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.2.0 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.3 + react-redux: 9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1) + + '@remixicon/react@4.8.0(react@19.2.3)': + dependencies: + react: 19.2.3 - '@rollup/plugin-commonjs@28.0.6(rollup@4.34.8)': + '@rollup/plugin-commonjs@29.0.0(rollup@4.52.5)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.34.8) + '@rollup/pluginutils': 5.3.0(rollup@4.52.5) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.4.3(picomatch@4.0.2) + fdir: 6.5.0(picomatch@4.0.3) is-reference: 1.2.1 - magic-string: 0.30.17 - picomatch: 4.0.2 + magic-string: 0.30.21 + picomatch: 4.0.3 optionalDependencies: - rollup: 4.34.8 + rollup: 4.52.5 - '@rollup/pluginutils@5.1.4(rollup@4.34.8)': + '@rollup/pluginutils@5.3.0(rollup@4.52.5)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.2 + picomatch: 4.0.3 optionalDependencies: - rollup: 4.34.8 + rollup: 4.52.5 - '@rollup/rollup-android-arm-eabi@4.34.8': + '@rollup/rollup-android-arm-eabi@4.52.5': optional: true - '@rollup/rollup-android-arm64@4.34.8': + '@rollup/rollup-android-arm64@4.52.5': optional: true - '@rollup/rollup-darwin-arm64@4.34.8': + '@rollup/rollup-darwin-arm64@4.52.5': optional: true - '@rollup/rollup-darwin-x64@4.34.8': + '@rollup/rollup-darwin-x64@4.52.5': optional: true - '@rollup/rollup-freebsd-arm64@4.34.8': + '@rollup/rollup-freebsd-arm64@4.52.5': optional: true - '@rollup/rollup-freebsd-x64@4.34.8': + '@rollup/rollup-freebsd-x64@4.52.5': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.8': + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.8': + '@rollup/rollup-linux-arm-musleabihf@4.52.5': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.8': + '@rollup/rollup-linux-arm64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.8': + '@rollup/rollup-linux-arm64-musl@4.52.5': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.8': + '@rollup/rollup-linux-loong64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': + '@rollup/rollup-linux-ppc64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.8': + '@rollup/rollup-linux-riscv64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.8': + '@rollup/rollup-linux-riscv64-musl@4.52.5': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.8': + '@rollup/rollup-linux-s390x-gnu@4.52.5': optional: true - '@rollup/rollup-linux-x64-musl@4.34.8': + '@rollup/rollup-linux-x64-gnu@4.52.5': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.8': + '@rollup/rollup-linux-x64-musl@4.52.5': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.8': + '@rollup/rollup-openharmony-arm64@4.52.5': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.8': + '@rollup/rollup-win32-arm64-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true '@rtsao/scc@1.1.0': {} '@scarf/scarf@1.4.0': {} - '@sec-ant/readable-stream@0.4.1': {} - '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 selderee: 0.11.0 optional: true - '@sinclair/typebox@0.34.38': {} - - '@sindresorhus/is@5.6.0': {} - - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@13.0.5': - dependencies: - '@sinonjs/commons': 3.0.1 - - '@smithy/abort-controller@4.0.5': - dependencies: - '@smithy/types': 4.4.0 - tslib: 2.8.1 - - '@smithy/abort-controller@4.1.0': - dependencies: - '@smithy/types': 4.4.0 - tslib: 2.8.1 - - '@smithy/chunked-blob-reader-native@4.0.0': - dependencies: - '@smithy/util-base64': 4.1.0 - tslib: 2.8.1 - - '@smithy/chunked-blob-reader@5.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/config-resolver@4.1.5': - dependencies: - '@smithy/node-config-provider': 4.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-config-provider': 4.0.0 - '@smithy/util-middleware': 4.1.0 - tslib: 2.8.1 - - '@smithy/config-resolver@4.2.0': - dependencies: - '@smithy/node-config-provider': 4.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-config-provider': 4.1.0 - '@smithy/util-middleware': 4.1.0 - tslib: 2.8.1 - - '@smithy/core@3.10.0': - dependencies: - '@smithy/middleware-serde': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-stream': 4.3.0 - '@smithy/util-utf8': 4.1.0 - '@types/uuid': 9.0.8 - tslib: 2.8.1 - uuid: 9.0.1 - - '@smithy/core@3.8.0': - dependencies: - '@smithy/middleware-serde': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-body-length-browser': 4.1.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-stream': 4.3.0 - '@smithy/util-utf8': 4.1.0 - '@types/uuid': 9.0.8 - tslib: 2.8.1 - uuid: 9.0.1 - - '@smithy/credential-provider-imds@4.0.7': - dependencies: - '@smithy/node-config-provider': 4.2.0 - '@smithy/property-provider': 4.1.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.1.0': - dependencies: - '@smithy/node-config-provider': 4.2.0 - '@smithy/property-provider': 4.1.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - tslib: 2.8.1 - - '@smithy/eventstream-codec@4.0.5': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-hex-encoding': 4.1.0 - tslib: 2.8.1 - - '@smithy/eventstream-serde-browser@4.0.5': - dependencies: - '@smithy/eventstream-serde-universal': 4.0.5 - '@smithy/types': 4.4.0 - tslib: 2.8.1 - - '@smithy/eventstream-serde-config-resolver@4.1.3': - dependencies: - '@smithy/types': 4.4.0 - tslib: 2.8.1 - - '@smithy/eventstream-serde-node@4.0.5': - dependencies: - '@smithy/eventstream-serde-universal': 4.0.5 - '@smithy/types': 4.4.0 - tslib: 2.8.1 - - '@smithy/eventstream-serde-universal@4.0.5': - dependencies: - '@smithy/eventstream-codec': 4.0.5 - '@smithy/types': 4.4.0 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.1.1': + '@sideway/address@4.1.5': dependencies: - '@smithy/protocol-http': 5.2.0 - '@smithy/querystring-builder': 4.0.5 - '@smithy/types': 4.4.0 - '@smithy/util-base64': 4.1.0 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.2.0': - dependencies: - '@smithy/protocol-http': 5.2.0 - '@smithy/querystring-builder': 4.1.0 - '@smithy/types': 4.4.0 - '@smithy/util-base64': 4.1.0 - tslib: 2.8.1 + '@hapi/hoek': 9.3.0 - '@smithy/hash-blob-browser@4.0.5': - dependencies: - '@smithy/chunked-blob-reader': 5.0.0 - '@smithy/chunked-blob-reader-native': 4.0.0 - '@smithy/types': 4.4.0 - tslib: 2.8.1 + '@sideway/formula@3.0.1': {} - '@smithy/hash-node@4.0.5': - dependencies: - '@smithy/types': 4.4.0 - '@smithy/util-buffer-from': 4.1.0 - '@smithy/util-utf8': 4.1.0 - tslib: 2.8.1 + '@sideway/pinpoint@2.0.0': {} - '@smithy/hash-stream-node@4.0.5': - dependencies: - '@smithy/types': 4.4.0 - '@smithy/util-utf8': 4.1.0 - tslib: 2.8.1 + '@sinclair/typebox@0.27.8': {} - '@smithy/invalid-dependency@4.0.5': - dependencies: - '@smithy/types': 4.4.0 - tslib: 2.8.1 + '@sinclair/typebox@0.34.41': {} - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 + '@sindresorhus/is@4.6.0': {} - '@smithy/is-array-buffer@4.1.0': - dependencies: - tslib: 2.8.1 + '@sindresorhus/is@5.6.0': {} - '@smithy/md5-js@4.0.5': + '@sinonjs/commons@3.0.1': dependencies: - '@smithy/types': 4.4.0 - '@smithy/util-utf8': 4.1.0 - tslib: 2.8.1 + type-detect: 4.0.8 - '@smithy/middleware-content-length@4.0.5': + '@sinonjs/fake-timers@13.0.5': dependencies: - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - tslib: 2.8.1 + '@sinonjs/commons': 3.0.1 - '@smithy/middleware-endpoint@4.1.18': + '@slorber/react-helmet-async@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@smithy/core': 3.10.0 - '@smithy/middleware-serde': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-middleware': 4.1.0 - tslib: 2.8.1 + '@babel/runtime': 7.28.4 + invariant: 2.2.4 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 - '@smithy/middleware-endpoint@4.2.0': + '@slorber/remark-comment@1.0.0': dependencies: - '@smithy/core': 3.10.0 - '@smithy/middleware-serde': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 - '@smithy/url-parser': 4.1.0 - '@smithy/util-middleware': 4.1.0 - tslib: 2.8.1 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 - '@smithy/middleware-retry@4.1.19': + '@smithy/abort-controller@4.2.8': dependencies: - '@smithy/node-config-provider': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/service-error-classification': 4.0.7 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-retry': 4.1.0 - '@types/uuid': 9.0.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - uuid: 9.0.1 - '@smithy/middleware-retry@4.2.0': + '@smithy/chunked-blob-reader-native@4.2.1': dependencies: - '@smithy/node-config-provider': 4.2.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/service-error-classification': 4.1.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-retry': 4.1.0 - '@types/uuid': 9.0.8 + '@smithy/util-base64': 4.3.0 tslib: 2.8.1 - uuid: 9.0.1 - '@smithy/middleware-serde@4.0.9': + '@smithy/chunked-blob-reader@5.2.0': dependencies: - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/middleware-serde@4.1.0': + '@smithy/config-resolver@4.4.6': dependencies: - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-config-provider': 4.2.0 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/middleware-stack@4.0.5': - dependencies: - '@smithy/types': 4.4.0 + '@smithy/core@3.20.6': + dependencies: + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-body-length-browser': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 + '@smithy/util-utf8': 4.2.0 + '@smithy/uuid': 1.1.0 tslib: 2.8.1 - '@smithy/middleware-stack@4.1.0': + '@smithy/credential-provider-imds@4.2.8': dependencies: - '@smithy/types': 4.4.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 tslib: 2.8.1 - '@smithy/node-config-provider@4.1.4': + '@smithy/eventstream-codec@4.2.8': dependencies: - '@smithy/property-provider': 4.0.5 - '@smithy/shared-ini-file-loader': 4.0.5 - '@smithy/types': 4.4.0 + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 tslib: 2.8.1 - '@smithy/node-config-provider@4.2.0': + '@smithy/eventstream-serde-browser@4.2.8': dependencies: - '@smithy/property-provider': 4.1.0 - '@smithy/shared-ini-file-loader': 4.1.0 - '@smithy/types': 4.4.0 + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.1.1': + '@smithy/eventstream-serde-config-resolver@4.3.8': dependencies: - '@smithy/abort-controller': 4.0.5 - '@smithy/protocol-http': 5.2.0 - '@smithy/querystring-builder': 4.0.5 - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.2.0': + '@smithy/eventstream-serde-node@4.2.8': dependencies: - '@smithy/abort-controller': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/querystring-builder': 4.1.0 - '@smithy/types': 4.4.0 + '@smithy/eventstream-serde-universal': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/property-provider@4.0.5': + '@smithy/eventstream-serde-universal@4.2.8': dependencies: - '@smithy/types': 4.4.0 + '@smithy/eventstream-codec': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/property-provider@4.1.0': + '@smithy/fetch-http-handler@5.3.9': dependencies: - '@smithy/types': 4.4.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 tslib: 2.8.1 - '@smithy/protocol-http@5.1.3': + '@smithy/hash-blob-browser@4.2.9': dependencies: - '@smithy/types': 4.4.0 + '@smithy/chunked-blob-reader': 5.2.0 + '@smithy/chunked-blob-reader-native': 4.2.1 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/protocol-http@5.2.0': + '@smithy/hash-node@4.2.8': dependencies: - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/querystring-builder@4.0.5': + '@smithy/hash-stream-node@4.2.8': dependencies: - '@smithy/types': 4.4.0 - '@smithy/util-uri-escape': 4.0.0 + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/querystring-builder@4.1.0': + '@smithy/invalid-dependency@4.2.8': dependencies: - '@smithy/types': 4.4.0 - '@smithy/util-uri-escape': 4.1.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/querystring-parser@4.0.5': + '@smithy/is-array-buffer@2.2.0': dependencies: - '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/querystring-parser@4.1.0': + '@smithy/is-array-buffer@4.2.0': dependencies: - '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/service-error-classification@4.0.7': + '@smithy/md5-js@4.2.8': dependencies: - '@smithy/types': 4.4.0 - - '@smithy/service-error-classification@4.1.0': - dependencies: - '@smithy/types': 4.4.0 - - '@smithy/shared-ini-file-loader@4.0.5': - dependencies: - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/shared-ini-file-loader@4.1.0': + '@smithy/middleware-content-length@4.2.8': dependencies: - '@smithy/types': 4.4.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/signature-v4@5.1.3': + '@smithy/middleware-endpoint@4.4.7': dependencies: - '@smithy/is-array-buffer': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-hex-encoding': 4.1.0 - '@smithy/util-middleware': 4.1.0 - '@smithy/util-uri-escape': 4.1.0 - '@smithy/util-utf8': 4.1.0 + '@smithy/core': 3.20.6 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/smithy-client@4.4.10': + '@smithy/middleware-retry@4.4.23': dependencies: - '@smithy/core': 3.10.0 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/middleware-stack': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-stream': 4.3.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 + '@smithy/uuid': 1.1.0 tslib: 2.8.1 - '@smithy/smithy-client@4.6.0': + '@smithy/middleware-serde@4.2.9': dependencies: - '@smithy/core': 3.10.0 - '@smithy/middleware-endpoint': 4.2.0 - '@smithy/middleware-stack': 4.1.0 - '@smithy/protocol-http': 5.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-stream': 4.3.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/types@4.3.2': + '@smithy/middleware-stack@4.2.8': dependencies: + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/types@4.4.0': + '@smithy/node-config-provider@4.3.8': dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/url-parser@4.0.5': + '@smithy/node-http-handler@4.4.8': dependencies: - '@smithy/querystring-parser': 4.0.5 - '@smithy/types': 4.4.0 + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/url-parser@4.1.0': + '@smithy/property-provider@4.2.8': dependencies: - '@smithy/querystring-parser': 4.1.0 - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-base64@4.0.0': + '@smithy/protocol-http@5.3.8': dependencies: - '@smithy/util-buffer-from': 4.0.0 - '@smithy/util-utf8': 4.1.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-base64@4.1.0': + '@smithy/querystring-builder@4.2.8': dependencies: - '@smithy/util-buffer-from': 4.1.0 - '@smithy/util-utf8': 4.1.0 + '@smithy/types': 4.12.0 + '@smithy/util-uri-escape': 4.2.0 tslib: 2.8.1 - '@smithy/util-body-length-browser@4.0.0': + '@smithy/querystring-parser@4.2.8': dependencies: + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-body-length-browser@4.1.0': + '@smithy/service-error-classification@4.2.8': dependencies: - tslib: 2.8.1 + '@smithy/types': 4.12.0 - '@smithy/util-body-length-node@4.0.0': + '@smithy/shared-ini-file-loader@4.4.3': dependencies: + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-buffer-from@2.2.0': + '@smithy/signature-v4@5.3.8': dependencies: - '@smithy/is-array-buffer': 2.2.0 + '@smithy/is-array-buffer': 4.2.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-uri-escape': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/util-buffer-from@4.0.0': + '@smithy/smithy-client@4.10.8': dependencies: - '@smithy/is-array-buffer': 4.1.0 + '@smithy/core': 3.20.6 + '@smithy/middleware-endpoint': 4.4.7 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@smithy/util-buffer-from@4.1.0': + '@smithy/types@4.12.0': dependencies: - '@smithy/is-array-buffer': 4.1.0 tslib: 2.8.1 - '@smithy/util-config-provider@4.0.0': + '@smithy/url-parser@4.2.8': dependencies: + '@smithy/querystring-parser': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-config-provider@4.1.0': + '@smithy/util-base64@4.3.0': dependencies: + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.0.26': + '@smithy/util-body-length-browser@4.2.0': dependencies: - '@smithy/property-provider': 4.0.5 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - bowser: 2.11.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.1.0': + '@smithy/util-body-length-node@4.2.1': dependencies: - '@smithy/property-provider': 4.1.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 - bowser: 2.11.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.0.26': + '@smithy/util-buffer-from@2.2.0': dependencies: - '@smithy/config-resolver': 4.2.0 - '@smithy/credential-provider-imds': 4.0.7 - '@smithy/node-config-provider': 4.2.0 - '@smithy/property-provider': 4.0.5 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 + '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.1.0': + '@smithy/util-buffer-from@4.2.0': dependencies: - '@smithy/config-resolver': 4.2.0 - '@smithy/credential-provider-imds': 4.1.0 - '@smithy/node-config-provider': 4.2.0 - '@smithy/property-provider': 4.1.0 - '@smithy/smithy-client': 4.6.0 - '@smithy/types': 4.4.0 + '@smithy/is-array-buffer': 4.2.0 tslib: 2.8.1 - '@smithy/util-endpoints@3.0.7': + '@smithy/util-config-provider@4.2.0': dependencies: - '@smithy/node-config-provider': 4.2.0 - '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/util-hex-encoding@4.1.0': + '@smithy/util-defaults-mode-browser@4.3.22': dependencies: + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-middleware@4.0.5': + '@smithy/util-defaults-mode-node@4.2.25': dependencies: - '@smithy/types': 4.4.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-middleware@4.1.0': + '@smithy/util-endpoints@3.2.8': dependencies: - '@smithy/types': 4.4.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-retry@4.0.7': + '@smithy/util-hex-encoding@4.2.0': dependencies: - '@smithy/service-error-classification': 4.0.7 - '@smithy/types': 4.4.0 tslib: 2.8.1 - '@smithy/util-retry@4.1.0': + '@smithy/util-middleware@4.2.8': dependencies: - '@smithy/service-error-classification': 4.1.0 - '@smithy/types': 4.4.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-stream@4.3.0': + '@smithy/util-retry@4.2.8': dependencies: - '@smithy/fetch-http-handler': 5.2.0 - '@smithy/node-http-handler': 4.2.0 - '@smithy/types': 4.4.0 - '@smithy/util-base64': 4.1.0 - '@smithy/util-buffer-from': 4.1.0 - '@smithy/util-hex-encoding': 4.1.0 - '@smithy/util-utf8': 4.1.0 + '@smithy/service-error-classification': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-uri-escape@4.0.0': + '@smithy/util-stream@4.5.10': dependencies: + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/types': 4.12.0 + '@smithy/util-base64': 4.3.0 + '@smithy/util-buffer-from': 4.2.0 + '@smithy/util-hex-encoding': 4.2.0 + '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/util-uri-escape@4.1.0': + '@smithy/util-uri-escape@4.2.0': dependencies: tslib: 2.8.1 @@ -15328,20 +18839,19 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.0.0': + '@smithy/util-utf8@4.2.0': dependencies: - '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-buffer-from': 4.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.1.0': + '@smithy/util-waiter@4.2.8': dependencies: - '@smithy/util-buffer-from': 4.1.0 + '@smithy/abort-controller': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-waiter@4.0.7': + '@smithy/uuid@1.1.0': dependencies: - '@smithy/abort-controller': 4.1.0 - '@smithy/types': 4.4.0 tslib: 2.8.1 '@sqltools/formatter@1.2.5': {} @@ -15350,56 +18860,56 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 - '@svgr/babel-preset@8.1.0(@babel/core@7.28.4)': + '@svgr/babel-preset@8.1.0(@babel/core@7.28.6)': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.4) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.6) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.6) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.6) - '@svgr/core@8.1.0(typescript@5.8.3)': + '@svgr/core@8.1.0(typescript@5.9.3)': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.6) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.8.3) + cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -15407,69 +18917,69 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) - '@svgr/core': 8.1.0(typescript@5.8.3) + '@babel/core': 7.28.6 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.6) + '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@svgr/core': 8.1.0(typescript@5.8.3) - cosmiconfig: 8.3.6(typescript@5.8.3) + '@svgr/core': 8.1.0(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 svgo: 3.3.2 transitivePeerDependencies: - typescript - '@svgr/webpack@8.1.0(typescript@5.8.3)': + '@svgr/webpack@8.1.0(typescript@5.9.3)': dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.28.4) - '@babel/preset-env': 7.28.0(@babel/core@7.28.4) - '@babel/preset-react': 7.27.1(@babel/core@7.28.4) - '@babel/preset-typescript': 7.27.1(@babel/core@7.28.4) - '@svgr/core': 8.1.0(typescript@5.8.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3) + '@babel/core': 7.28.6 + '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.28.6) + '@babel/preset-env': 7.28.5(@babel/core@7.28.6) + '@babel/preset-react': 7.28.5(@babel/core@7.28.6) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.6) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) transitivePeerDependencies: - supports-color - typescript - '@swc-node/core@1.14.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.24)': + '@swc-node/core@1.14.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(@swc/types@0.1.25)': dependencies: - '@swc/core': 1.13.5(@swc/helpers@0.5.17) - '@swc/types': 0.1.24 + '@swc/core': 1.13.5(@swc/helpers@0.5.18) + '@swc/types': 0.1.25 - '@swc-node/jest@1.9.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.24)(typescript@5.8.3)': + '@swc-node/jest@1.9.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3)': dependencies: '@node-rs/xxhash': 1.7.6 - '@swc-node/core': 1.14.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.24) - '@swc-node/register': 1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.24)(typescript@5.8.3) - '@swc/core': 1.13.5(@swc/helpers@0.5.17) - '@swc/types': 0.1.24 - typescript: 5.8.3 + '@swc-node/core': 1.14.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(@swc/types@0.1.25) + '@swc-node/register': 1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3) + '@swc/core': 1.13.5(@swc/helpers@0.5.18) + '@swc/types': 0.1.25 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.24)(typescript@5.8.3)': + '@swc-node/register@1.11.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3)': dependencies: - '@swc-node/core': 1.14.1(@swc/core@1.13.5(@swc/helpers@0.5.17))(@swc/types@0.1.24) + '@swc-node/core': 1.14.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(@swc/types@0.1.25) '@swc-node/sourcemap-support': 0.6.1 - '@swc/core': 1.13.5(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.18) colorette: 2.0.20 - debug: 4.4.1(supports-color@10.1.0) - oxc-resolver: 11.6.1 + debug: 4.4.3(supports-color@10.2.2) + oxc-resolver: 11.13.1 pirates: 4.0.7 tslib: 2.8.1 - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - '@swc/types' - supports-color @@ -15479,20 +18989,24 @@ snapshots: source-map-support: 0.5.21 tslib: 2.8.1 - '@swc/cli@0.7.8(@swc/core@1.13.5(@swc/helpers@0.5.17))(chokidar@4.0.3)': + '@swc/cli@0.7.10(@swc/core@1.13.5(@swc/helpers@0.5.18))(chokidar@4.0.3)': dependencies: - '@swc/core': 1.13.5(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.18) '@swc/counter': 0.1.3 - '@xhmikosr/bin-wrapper': 13.0.5 + '@xhmikosr/bin-wrapper': 13.2.0 commander: 8.3.0 minimatch: 9.0.5 - piscina: 4.7.0 - semver: 7.7.2 + piscina: 4.9.2 + semver: 7.7.3 slash: 3.0.0 - source-map: 0.7.4 - tinyglobby: 0.2.14 + source-map: 0.7.6 + tinyglobby: 0.2.15 optionalDependencies: chokidar: 4.0.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color '@swc/core-darwin-arm64@1.13.5': optional: true @@ -15524,10 +19038,10 @@ snapshots: '@swc/core-win32-x64-msvc@1.13.5': optional: true - '@swc/core@1.13.5(@swc/helpers@0.5.17)': + '@swc/core@1.13.5(@swc/helpers@0.5.18)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.24 + '@swc/types': 0.1.25 optionalDependencies: '@swc/core-darwin-arm64': 1.13.5 '@swc/core-darwin-x64': 1.13.5 @@ -15539,7 +19053,7 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.13.5 '@swc/core-win32-ia32-msvc': 1.13.5 '@swc/core-win32-x64-msvc': 1.13.5 - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.18 '@swc/counter@0.1.3': {} @@ -15547,18 +19061,18 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/helpers@0.5.17': + '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 - '@swc/jest@0.2.39(@swc/core@1.13.5(@swc/helpers@0.5.17))': + '@swc/jest@0.2.39(@swc/core@1.13.5(@swc/helpers@0.5.18))': dependencies: - '@jest/create-cache-key-function': 30.0.5 - '@swc/core': 1.13.5(@swc/helpers@0.5.17) + '@jest/create-cache-key-function': 30.2.0 + '@swc/core': 1.13.5(@swc/helpers@0.5.18) '@swc/counter': 0.1.3 jsonc-parser: 3.3.1 - '@swc/types@0.1.24': + '@swc/types@0.1.25': dependencies: '@swc/counter': 0.1.3 @@ -15566,90 +19080,95 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@t3-oss/env-core@0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@4.1.5)': + '@t3-oss/env-core@0.13.10(typescript@5.9.3)(zod@4.3.5)': optionalDependencies: - arktype: 2.1.20 - typescript: 5.8.3 - zod: 4.1.5 + typescript: 5.9.3 + zod: 4.3.5 - '@t3-oss/env-nextjs@0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@4.1.5)': + '@t3-oss/env-nextjs@0.13.10(typescript@5.9.3)(zod@4.3.5)': dependencies: - '@t3-oss/env-core': 0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@4.1.5) + '@t3-oss/env-core': 0.13.10(typescript@5.9.3)(zod@4.3.5) optionalDependencies: - arktype: 2.1.20 - typescript: 5.8.3 - zod: 4.1.5 + typescript: 5.9.3 + zod: 4.3.5 - '@tanstack/query-core@5.87.1': {} + '@tanstack/query-core@5.90.19': {} - '@tanstack/query-devtools@5.86.0': {} + '@tanstack/query-devtools@5.92.0': {} - '@tanstack/react-query-devtools@5.87.1(@tanstack/react-query@5.87.1(react@19.1.1))(react@19.1.1)': + '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.19(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/query-devtools': 5.86.0 - '@tanstack/react-query': 5.87.1(react@19.1.1) - react: 19.1.1 + '@tanstack/query-devtools': 5.92.0 + '@tanstack/react-query': 5.90.19(react@19.2.3) + react: 19.2.3 - '@tanstack/react-query@5.87.1(react@19.1.1)': + '@tanstack/react-query@5.90.19(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.87.1 - react: 19.1.1 + '@tanstack/query-core': 5.90.19 + react: 19.2.3 - '@tanstack/react-table@8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/table-core': 8.21.3 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) '@tanstack/table-core@8.21.3': {} - '@testing-library/dom@10.3.2': + '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.2 + '@babel/code-frame': 7.28.6 + '@babel/runtime': 7.28.4 '@types/aria-query': 5.0.4 aria-query: 5.3.0 - chalk: 4.1.2 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 + picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.8.0': + '@testing-library/jest-dom@6.9.1': dependencies: - '@adobe/css-tools': 4.4.0 + '@adobe/css-tools': 4.4.4 aria-query: 5.3.2 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.3.2)(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.27.1 - '@testing-library/dom': 10.3.2 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@babel/runtime': 7.28.4 + '@testing-library/dom': 10.4.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 - '@types/react-dom': 19.1.9(@types/react@19.1.12) + '@types/react': 19.2.8 + '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@testing-library/user-event@14.6.1(@testing-library/dom@10.3.2)': + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: - '@testing-library/dom': 10.3.2 + '@testing-library/dom': 10.4.1 '@tokenizer/inflate@0.2.7': dependencies: - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) fflate: 0.8.2 - token-types: 6.0.0 + token-types: 6.1.1 + transitivePeerDependencies: + - supports-color + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3(supports-color@10.2.2) + token-types: 6.1.1 transitivePeerDependencies: - supports-color '@tokenizer/token@0.3.0': {} - '@toss/use-overlay@1.4.2(react@19.1.1)': + '@toss/use-overlay@1.4.2(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 '@trysound/sax@0.2.0': {} @@ -15661,7 +19180,7 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@tybys/wasm-util@0.10.0': + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true @@ -15670,45 +19189,54 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - '@types/babel__generator': 7.6.8 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 + '@types/babel__traverse': 7.28.0 - '@types/babel__generator@7.6.8': + '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@types/babel__traverse@7.20.6': + '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 '@types/bcrypt@6.0.0': dependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 - '@types/body-parser@1.19.5': + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.18.1 + '@types/node': 24.10.8 + + '@types/bonjour@3.5.13': + dependencies: + '@types/node': 24.10.8 + + '@types/cls-hooked@4.3.9': + dependencies: + '@types/node': 24.10.8 - '@types/cls-hooked@4.3.8': + '@types/connect-history-api-fallback@1.5.4': dependencies: - '@types/node': 22.18.1 + '@types/express-serve-static-core': 5.1.0 + '@types/node': 24.10.8 '@types/connect@3.4.38': dependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 '@types/cookiejar@2.1.5': {} - '@types/d3-array@3.2.1': {} + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -15718,20 +19246,24 @@ snapshots: dependencies: '@types/d3-color': 3.1.3 - '@types/d3-path@3.1.0': {} + '@types/d3-path@3.1.1': {} - '@types/d3-scale@4.0.8': + '@types/d3-scale@4.0.9': dependencies: '@types/d3-time': 3.0.4 - '@types/d3-shape@3.1.6': + '@types/d3-shape@3.1.7': dependencies: - '@types/d3-path': 3.1.0 + '@types/d3-path': 3.1.1 '@types/d3-time@3.0.4': {} '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/ejs@3.1.5': optional: true @@ -15745,31 +19277,61 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 - '@types/estree@1.0.6': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 '@types/estree@1.0.8': {} - '@types/express-serve-static-core@5.0.6': + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 24.10.8 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 22.18.1 - '@types/qs': 6.9.18 + '@types/node': 24.10.8 + '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 - '@types/express@5.0.3': + '@types/express@5.0.6': dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 5.0.6 - '@types/serve-static': 1.15.7 + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.0 + '@types/serve-static': 2.2.0 - '@types/hoist-non-react-statics@3.3.6': + '@types/gtag.js@0.0.12': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/history@4.7.11': {} + + '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.8)': dependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 hoist-non-react-statics: 3.3.2 + '@types/html-minifier-terser@6.1.0': {} + '@types/http-cache-semantics@4.0.4': {} - '@types/http-errors@2.0.4': {} + '@types/http-errors@2.0.5': {} + + '@types/http-proxy@1.17.17': + dependencies: + '@types/node': 24.10.8 '@types/istanbul-lib-coverage@2.0.6': {} @@ -15783,14 +19345,14 @@ snapshots: '@types/jest@30.0.0': dependencies: - expect: 30.0.5 - pretty-format: 30.0.5 + expect: 30.2.0 + pretty-format: 30.2.0 '@types/js-cookie@2.2.7': {} '@types/jsdom@21.1.7': dependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -15798,40 +19360,49 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonwebtoken@9.0.5': + '@types/jsonwebtoken@9.0.10': dependencies: - '@types/node': 22.18.1 + '@types/ms': 2.1.0 + '@types/node': 24.10.8 - '@types/jsonwebtoken@9.0.7': - dependencies: - '@types/node': 22.18.1 + '@types/luxon@3.7.1': {} - '@types/luxon@3.6.2': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 - '@types/luxon@3.7.1': {} + '@types/mdx@2.0.13': {} '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} - '@types/mjml-core@4.7.4': + '@types/mjml-core@4.15.2': optional: true '@types/mjml@4.7.4': dependencies: - '@types/mjml-core': 4.7.4 + '@types/mjml-core': 4.15.2 optional: true + '@types/ms@2.1.0': {} + + '@types/node-forge@1.3.14': + dependencies: + '@types/node': 24.10.8 + '@types/node@14.18.63': {} - '@types/node@22.18.1': + '@types/node@17.0.45': {} + + '@types/node@24.10.8': dependencies: - undici-types: 6.21.0 + undici-types: 7.16.0 - '@types/nodemailer@7.0.1': + '@types/nodemailer@7.0.5': dependencies: - '@aws-sdk/client-sesv2': 3.864.0 - '@types/node': 22.18.1 + '@aws-sdk/client-sesv2': 3.925.0 + '@types/node': 24.10.8 transitivePeerDependencies: - aws-crt @@ -15839,58 +19410,104 @@ snapshots: '@types/passport-jwt@4.0.1': dependencies: - '@types/jsonwebtoken': 9.0.5 + '@types/jsonwebtoken': 9.0.10 '@types/passport-strategy': 0.2.38 '@types/passport-local@1.0.38': dependencies: - '@types/express': 5.0.3 - '@types/passport': 1.0.16 + '@types/express': 5.0.6 + '@types/passport': 1.0.17 '@types/passport-strategy': 0.2.38 '@types/passport-strategy@0.2.38': dependencies: - '@types/express': 5.0.3 - '@types/passport': 1.0.16 + '@types/express': 5.0.6 + '@types/passport': 1.0.17 - '@types/passport@1.0.16': + '@types/passport@1.0.17': dependencies: - '@types/express': 5.0.3 + '@types/express': 5.0.6 + + '@types/prismjs@1.26.5': {} '@types/prompts@2.4.9': dependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 kleur: 3.0.3 '@types/pug@2.0.10': optional: true - '@types/qs@6.9.18': {} + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} - '@types/react-dom@19.1.9(@types/react@19.1.12)': + '@types/react-dom@19.2.3(@types/react@19.2.8)': + dependencies: + '@types/react': 19.2.8 + + '@types/react-router-config@5.0.11': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.2.8 + '@types/react-router': 5.1.20 + + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.2.8 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.2.8 + + '@types/react-transition-group@4.4.12(@types/react@19.2.8)': dependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - '@types/react-transition-group@4.4.12(@types/react@19.1.12)': + '@types/react@19.2.6': dependencies: - '@types/react': 19.1.12 + csstype: 3.2.3 - '@types/react@19.1.12': + '@types/react@19.2.8': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 - '@types/send@0.17.4': + '@types/retry@0.12.2': {} + + '@types/sax@1.2.7': + dependencies: + '@types/node': 24.10.8 + + '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.18.1 + '@types/node': 24.10.8 + + '@types/send@1.2.1': + dependencies: + '@types/node': 24.10.8 + + '@types/serve-index@1.9.4': + dependencies: + '@types/express': 5.0.6 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.10.8 + '@types/send': 0.17.6 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.10.8 - '@types/serve-static@1.15.7': + '@types/sockjs@0.3.36': dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 22.18.1 - '@types/send': 0.17.4 + '@types/node': 24.10.8 '@types/stack-utils@2.0.3': {} @@ -15898,8 +19515,8 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.18.1 - form-data: 4.0.2 + '@types/node': 24.10.8 + form-data: 4.0.4 '@types/supertest@6.0.3': dependencies: @@ -15908,107 +19525,115 @@ snapshots: '@types/tough-cookie@4.0.5': {} - '@types/uuid@9.0.8': {} + '@types/unist@2.0.11': {} - '@types/validator@13.12.0': {} + '@types/unist@3.0.3': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/validator@13.15.4': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.10.8 '@types/yargs-parser@21.0.3': {} - '@types/yargs@17.0.33': + '@types/yargs@17.0.34': dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.43.0 - '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.43.0 - eslint: 9.35.0(jiti@2.4.2) + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.3 + eslint: 9.39.2(jiti@1.21.7) graphemer: 1.4.0 - ignore: 7.0.4 + ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.43.0 - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.43.0 - debug: 4.4.1(supports-color@10.1.0) - eslint: 9.35.0(jiti@2.4.2) - typescript: 5.8.3 + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.3 + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.2(jiti@1.21.7) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.43.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.46.3(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) - '@typescript-eslint/types': 8.43.0 - debug: 4.4.1(supports-color@10.1.0) - typescript: 5.8.3 + '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + debug: 4.4.3(supports-color@10.2.2) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.43.0': + '@typescript-eslint/scope-manager@8.46.3': dependencies: - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/visitor-keys': 8.43.0 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/visitor-keys': 8.46.3 - '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)': dependencies: - typescript: 5.8.3 + typescript: 5.9.3 - '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1(supports-color@10.1.0) - eslint: 9.35.0(jiti@2.4.2) - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.2(jiti@1.21.7) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.43.0': {} + '@typescript-eslint/types@8.46.3': {} - '@typescript-eslint/typescript-estree@8.43.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.46.3(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.43.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.3) - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/visitor-keys': 8.43.0 - debug: 4.4.1(supports-color@10.1.0) + '@typescript-eslint/project-service': 8.46.3(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/visitor-keys': 8.46.3 + debug: 4.4.3(supports-color@10.2.2) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + semver: 7.7.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.43.0 - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) - eslint: 9.35.0(jiti@2.4.2) - typescript: 5.8.3 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.46.3 + '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.43.0': + '@typescript-eslint/visitor-keys@8.46.3': dependencies: - '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/types': 8.46.3 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -16148,74 +19773,101 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@willsoto/nestjs-prometheus@6.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3)': + '@willsoto/nestjs-prometheus@6.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3)': dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) prom-client: 15.1.3 - '@xhmikosr/archive-type@7.0.0': + '@xhmikosr/archive-type@7.1.0': dependencies: - file-type: 19.6.0 + file-type: 20.5.0 + transitivePeerDependencies: + - supports-color - '@xhmikosr/bin-check@7.0.3': + '@xhmikosr/bin-check@7.1.0': dependencies: execa: 5.1.1 isexe: 2.0.0 - '@xhmikosr/bin-wrapper@13.0.5': + '@xhmikosr/bin-wrapper@13.2.0': dependencies: - '@xhmikosr/bin-check': 7.0.3 - '@xhmikosr/downloader': 15.0.1 + '@xhmikosr/bin-check': 7.1.0 + '@xhmikosr/downloader': 15.2.0 '@xhmikosr/os-filter-obj': 3.0.0 bin-version-check: 5.1.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color - '@xhmikosr/decompress-tar@8.0.1': + '@xhmikosr/decompress-tar@8.1.0': dependencies: - file-type: 19.6.0 + file-type: 20.5.0 is-stream: 2.0.1 tar-stream: 3.1.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color - '@xhmikosr/decompress-tarbz2@8.0.1': + '@xhmikosr/decompress-tarbz2@8.1.0': dependencies: - '@xhmikosr/decompress-tar': 8.0.1 - file-type: 19.6.0 + '@xhmikosr/decompress-tar': 8.1.0 + file-type: 20.5.0 is-stream: 2.0.1 seek-bzip: 2.0.0 unbzip2-stream: 1.4.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color - '@xhmikosr/decompress-targz@8.0.1': + '@xhmikosr/decompress-targz@8.1.0': dependencies: - '@xhmikosr/decompress-tar': 8.0.1 - file-type: 19.6.0 + '@xhmikosr/decompress-tar': 8.1.0 + file-type: 20.5.0 is-stream: 2.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color - '@xhmikosr/decompress-unzip@7.0.0': + '@xhmikosr/decompress-unzip@7.1.0': dependencies: - file-type: 19.6.0 + file-type: 20.5.0 get-stream: 6.0.1 yauzl: 3.2.0 + transitivePeerDependencies: + - supports-color - '@xhmikosr/decompress@10.0.1': + '@xhmikosr/decompress@10.2.0': dependencies: - '@xhmikosr/decompress-tar': 8.0.1 - '@xhmikosr/decompress-tarbz2': 8.0.1 - '@xhmikosr/decompress-targz': 8.0.1 - '@xhmikosr/decompress-unzip': 7.0.0 + '@xhmikosr/decompress-tar': 8.1.0 + '@xhmikosr/decompress-tarbz2': 8.1.0 + '@xhmikosr/decompress-targz': 8.1.0 + '@xhmikosr/decompress-unzip': 7.1.0 graceful-fs: 4.2.11 - make-dir: 4.0.0 strip-dirs: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color - '@xhmikosr/downloader@15.0.1': + '@xhmikosr/downloader@15.2.0': dependencies: - '@xhmikosr/archive-type': 7.0.0 - '@xhmikosr/decompress': 10.0.1 + '@xhmikosr/archive-type': 7.1.0 + '@xhmikosr/decompress': 10.2.0 content-disposition: 0.5.4 - defaults: 3.0.0 + defaults: 2.0.2 ext-name: 5.0.0 - file-type: 19.6.0 + file-type: 20.5.0 filenamify: 6.0.0 get-stream: 6.0.1 got: 13.0.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color '@xhmikosr/os-filter-obj@3.0.0': dependencies: @@ -16227,11 +19879,23 @@ snapshots: '@xtuc/long@4.2.2': {} + '@zone-eu/mailsplit@5.4.7': + dependencies: + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + optional: true + abbrev@2.0.0: optional: true abstract-logging@2.0.1: {} + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -16245,20 +19909,23 @@ snapshots: dependencies: acorn: 8.15.0 - acorn-walk@8.3.3: + acorn-walk@8.3.4: dependencies: - acorn: 8.12.1 + acorn: 8.15.0 acorn@7.4.1: optional: true - acorn@8.12.1: {} + acorn@8.15.0: {} - acorn@8.14.1: {} + address@1.2.2: {} - acorn@8.15.0: {} + agent-base@7.1.4: {} - agent-base@7.1.3: {} + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: @@ -16287,7 +19954,7 @@ snapshots: ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 + fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -16297,6 +19964,28 @@ snapshots: estraverse: 1.9.3 optional: true + algoliasearch-helper@3.26.1(algoliasearch@5.43.0): + dependencies: + '@algolia/events': 4.0.1 + algoliasearch: 5.43.0 + + algoliasearch@5.43.0: + dependencies: + '@algolia/abtesting': 1.9.0 + '@algolia/client-abtesting': 5.43.0 + '@algolia/client-analytics': 5.43.0 + '@algolia/client-common': 5.43.0 + '@algolia/client-insights': 5.43.0 + '@algolia/client-personalization': 5.43.0 + '@algolia/client-query-suggestions': 5.43.0 + '@algolia/client-search': 5.43.0 + '@algolia/ingestion': 1.43.0 + '@algolia/monitoring': 1.43.0 + '@algolia/recommend': 5.43.0 + '@algolia/requester-browser-xhr': 5.43.0 + '@algolia/requester-fetch': 5.43.0 + '@algolia/requester-node-http': 5.43.0 + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -16307,9 +19996,11 @@ snapshots: dependencies: type-fest: 0.21.3 + ansi-html-community@0.0.8: {} + ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: @@ -16317,11 +20008,9 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} - ansis@3.17.0: {} - - ansis@4.1.0: {} + ansis@4.2.0: {} any-promise@1.3.0: {} @@ -16365,7 +20054,7 @@ snapshots: archiver@5.3.2: dependencies: archiver-utils: 2.1.0 - async: 3.2.5 + async: 3.2.6 buffer-crc32: 0.2.13 readable-stream: 3.6.2 readdir-glob: 1.1.3 @@ -16382,7 +20071,7 @@ snapshots: argparse@2.0.1: {} - aria-hidden@1.2.4: + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -16392,25 +20081,12 @@ snapshots: aria-query@5.3.2: {} - arktype@2.1.20: - dependencies: - '@ark/schema': 0.46.0 - '@ark/util': 0.46.0 - optional: true - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 + array-flatten@1.1.1: {} array-includes@3.1.9: dependencies: @@ -16425,21 +20101,23 @@ snapshots: array-timsort@1.0.3: {} + array-union@2.1.0: {} + array.prototype.findlast@1.2.5: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -16448,86 +20126,86 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.0.2 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.0.2 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 array.prototype.tosorted@1.1.4: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 - es-shim-unscopables: 1.0.2 + es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 asap@2.0.6: {} - assert-never@1.3.0: + assert-never@1.4.0: optional: true ast-types-flow@0.0.8: {} + astring@1.9.0: {} + + async-function@1.0.0: {} + async-hook-jl@1.7.6: dependencies: stack-chain: 1.3.7 - async@3.2.5: {} - - async@3.2.6: - optional: true + async@3.2.6: {} asynckit@0.4.0: {} atomic-sleep@1.0.0: {} - autoprefixer@10.4.21(postcss@8.5.6): + autoprefixer@10.4.23(postcss@8.5.6): dependencies: - browserslist: 4.24.4 - caniuse-lite: 1.0.30001707 - fraction.js: 4.3.7 - normalize-range: 0.1.2 + browserslist: 4.28.1 + caniuse-lite: 1.0.30001760 + fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: dependencies: - possible-typed-array-names: 1.0.0 + possible-typed-array-names: 1.1.0 avvio@9.1.0: dependencies: - '@fastify/error': 4.1.0 + '@fastify/error': 4.2.0 fastq: 1.19.1 aws-ssl-profiles@1.1.2: {} aws4@1.13.2: {} - axe-core@4.10.3: {} + axe-core@4.11.0: {} - axios-auth-refresh@3.3.6(axios@1.11.0): + axios-auth-refresh@3.3.6(axios@1.13.2): dependencies: - axios: 1.11.0 + axios: 1.13.2 - axios@1.11.0: + axios@1.13.2: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.11 form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16535,22 +20213,33 @@ snapshots: axobject-query@4.1.0: {} - b4a@1.6.7: {} + b4a@1.7.3: {} - babel-jest@30.1.2(@babel/core@7.28.4): + babel-jest@30.2.0(@babel/core@7.28.6): dependencies: - '@babel/core': 7.28.4 - '@jest/transform': 30.1.2 + '@babel/core': 7.28.6 + '@jest/transform': 30.2.0 '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.0 - babel-preset-jest: 30.0.1(@babel/core@7.28.4) + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.2.0(@babel/core@7.28.6) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-plugin-istanbul@7.0.0: + babel-loader@9.2.1(@babel/core@7.28.6)(webpack@5.104.1): + dependencies: + '@babel/core': 7.28.6 + find-cache-dir: 4.0.0 + schema-utils: 4.3.3 + webpack: 5.104.1 + + babel-plugin-dynamic-import-node@2.3.3: + dependencies: + object.assign: 4.1.7 + + babel-plugin-istanbul@7.0.1: dependencies: '@babel/helper-plugin-utils': 7.27.1 '@istanbuljs/load-nyc-config': 1.1.0 @@ -16560,90 +20249,95 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-jest-hoist@30.0.1: + babel-plugin-jest-hoist@30.2.0: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 '@types/babel__core': 7.20.5 babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 - resolve: 1.22.10 + resolve: 1.22.11 - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.6): dependencies: - '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.4 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + '@babel/compat-data': 7.28.5 + '@babel/core': 7.28.6 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.6) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.4): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.6): dependencies: - '@babel/core': 7.28.4 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) - core-js-compat: 3.45.0 + '@babel/core': 7.28.6 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.6) + core-js-compat: 3.46.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.4): + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.6): dependencies: - '@babel/core': 7.28.4 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.4) + '@babel/core': 7.28.6 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.6) transitivePeerDependencies: - supports-color - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.4) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.4) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.4) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.4) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.4) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.4) - - babel-preset-jest@30.0.1(@babel/core@7.28.4): - dependencies: - '@babel/core': 7.28.4 - babel-plugin-jest-hoist: 30.0.1 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.6): + dependencies: + '@babel/core': 7.28.6 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.6) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.6) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.6) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.6) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.6) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.6) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.6) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.6) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.6) + + babel-preset-jest@30.2.0(@babel/core@7.28.6): + dependencies: + '@babel/core': 7.28.6 + babel-plugin-jest-hoist: 30.2.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.6) babel-walk@3.0.0-canary-5: dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.28.5 optional: true + bail@2.0.2: {} + balanced-match@1.0.2: {} - bare-events@2.5.0: - optional: true + bare-events@2.8.1: {} base64-js@1.5.1: {} + baseline-browser-mapping@2.9.7: {} + + batch@0.6.1: {} + bcrypt@6.0.0: dependencies: - node-addon-api: 8.3.1 + node-addon-api: 8.5.0 node-gyp-build: 4.8.4 big-integer@1.6.52: {} + big.js@5.2.2: {} + bin-version-check@5.1.0: dependencies: bin-version: 6.0.0 - semver: 7.7.2 + semver: 7.7.3 semver-truncate: 3.0.0 bin-version@6.0.0: @@ -16668,23 +20362,45 @@ snapshots: bluebird@3.4.7: {} - body-parser@2.2.0: + body-parser@1.20.3: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1(supports-color@10.1.0) + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.1: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@10.2.2) + http-errors: 2.0.0 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 + qs: 6.14.1 + raw-body: 3.0.1 type-is: 2.0.1 transitivePeerDependencies: - supports-color + bonjour-service@1.3.0: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + boolbase@1.0.0: {} - bowser@2.11.0: {} + bowser@2.12.1: {} boxen@5.1.2: dependencies: @@ -16697,12 +20413,34 @@ snapshots: widest-line: 3.1.0 wrap-ansi: 7.0.0 - brace-expansion@1.1.11: + boxen@6.2.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + + boxen@7.1.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -16710,19 +20448,21 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.24.4: + browserslist@4.27.0: dependencies: - caniuse-lite: 1.0.30001707 - electron-to-chromium: 1.5.114 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.4) + baseline-browser-mapping: 2.9.7 + caniuse-lite: 1.0.30001760 + electron-to-chromium: 1.5.245 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.27.0) - browserslist@4.25.1: + browserslist@4.28.1: dependencies: - caniuse-lite: 1.0.30001731 - electron-to-chromium: 1.5.195 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) + baseline-browser-mapping: 2.9.7 + caniuse-lite: 1.0.30001760 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) bs-logger@0.2.6: dependencies: @@ -16752,15 +20492,21 @@ snapshots: buffers@0.1.1: {} - bundle-require@5.1.0(esbuild@0.25.0): + bundle-name@4.1.0: dependencies: - esbuild: 0.25.0 + run-applescript: 7.1.0 + + bundle-require@5.1.0(esbuild@0.27.0): + dependencies: + esbuild: 0.27.0 load-tsconfig: 0.2.5 busboy@1.6.0: dependencies: streamsearch: 1.1.0 + bytes@3.0.0: {} + bytes@3.1.2: {} cac@6.7.14: {} @@ -16771,10 +20517,10 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.0.4 get-stream: 6.0.1 - http-cache-semantics: 4.1.1 + http-cache-semantics: 4.2.0 keyv: 4.5.4 mimic-response: 4.0.0 - normalize-url: 8.0.1 + normalize-url: 8.1.0 responselike: 3.0.0 call-bind-apply-helpers@1.0.2: @@ -16789,11 +20535,6 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -16807,15 +20548,29 @@ snapshots: upper-case: 1.1.3 optional: true + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + camelcase-css@2.0.1: {} camelcase@5.3.1: {} camelcase@6.3.0: {} - caniuse-lite@1.0.30001707: {} + camelcase@7.0.1: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001760 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001760: {} - caniuse-lite@1.0.30001731: {} + ccount@2.0.1: {} chainsaw@0.1.0: dependencies: @@ -16832,39 +20587,47 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + change-case@5.4.4: {} char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + character-parser@2.2.0: dependencies: is-regex: 1.2.1 optional: true - chardet@0.7.0: {} + character-reference-invalid@2.0.1: {} + + chardet@2.1.1: {} check-disk-space@3.4.0: {} cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 + css-select: 5.2.2 + css-what: 6.2.2 domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 - optional: true + domutils: 3.2.2 cheerio@1.0.0-rc.12: dependencies: cheerio-select: 2.1.0 dom-serializer: 2.0.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 htmlparser2: 8.0.2 parse5: 7.3.0 - parse5-htmlparser2-tree-adapter: 7.0.0 - optional: true + parse5-htmlparser2-tree-adapter: 7.1.0 chevrotain@11.0.3: dependencies: @@ -16895,20 +20658,19 @@ snapshots: chrome-trace-event@1.0.4: {} - ci-info@3.9.0: - optional: true + ci-info@3.9.0: {} - ci-info@4.3.0: {} + ci-info@4.3.1: {} cjs-module-lexer@2.1.0: {} class-transformer@0.5.1: {} - class-validator@0.14.2: + class-validator@0.14.3: dependencies: - '@types/validator': 13.12.0 - libphonenumber-js: 1.11.4 - validator: 13.12.0 + '@types/validator': 13.15.4 + libphonenumber-js: 1.12.25 + validator: 13.15.20 class-variance-authority@0.7.1: dependencies: @@ -16921,8 +20683,16 @@ snapshots: source-map: 0.6.1 optional: true + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + clean-stack@2.2.0: {} + cli-boxes@2.2.1: {} + cli-boxes@3.0.0: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -16945,6 +20715,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + clone@1.0.4: {} cls-hooked@4.2.2: @@ -16955,21 +20731,23 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1) - '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.8)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - '@types/react' - '@types/react-dom' co@4.6.0: {} - collect-v8-coverage@1.0.2: {} + collapse-white-space@2.1.0: {} + + collect-v8-coverage@1.0.3: {} color-convert@2.0.1: dependencies: @@ -16977,46 +20755,43 @@ snapshots: color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 + colord@2.9.3: {} colorette@1.4.0: {} colorette@2.0.20: {} + combine-promises@1.2.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - commander@10.0.1: - optional: true + comma-separated-tokens@2.0.3: {} + + commander@10.0.1: {} - commander@14.0.0: {} + commander@14.0.2: {} commander@2.20.3: {} commander@4.1.1: {} + commander@5.1.0: {} + commander@6.2.1: {} commander@7.2.0: {} commander@8.3.0: {} - comment-json@4.2.5: + comment-json@4.4.1: dependencies: array-timsort: 1.0.3 core-util-is: 1.0.3 esprima: 4.0.1 - has-own-prop: 2.0.0 - repeat-string: 1.6.1 + + common-path-prefix@3.0.0: {} commondir@1.0.1: {} @@ -17029,6 +20804,22 @@ snapshots: normalize-path: 3.0.0 readable-stream: 3.6.2 + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -17044,23 +20835,32 @@ snapshots: dependencies: ini: 1.3.8 proto-list: 1.2.4 - optional: true + + configstore@6.0.0: + dependencies: + dot-prop: 6.0.1 + graceful-fs: 4.2.11 + unique-string: 3.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 5.1.0 + + connect-history-api-fallback@2.0.0: {} consola@3.4.2: {} constantinople@4.0.1: dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 optional: true + content-disposition@0.5.2: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 + content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -17068,31 +20868,45 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} + cookie@0.7.2: {} cookie@1.0.2: {} cookiejar@2.1.4: {} - cookies-next@6.1.0(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + cookies-next@6.1.1(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: cookie: 1.0.2 - next: 15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 + next: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 - core-js-compat@3.45.0: + copy-webpack-plugin@11.0.0(webpack@5.104.1): + dependencies: + fast-glob: 3.3.3 + glob-parent: 6.0.2 + globby: 13.2.2 + normalize-path: 3.0.0 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + webpack: 5.104.1 + + core-js-compat@3.46.0: dependencies: - browserslist: 4.25.1 + browserslist: 4.28.1 - core-js-pure@3.39.0: {} + core-js-pure@3.46.0: {} - core-js@3.39.0: {} + core-js@3.46.0: {} core-util-is@1.0.3: {} @@ -17109,14 +20923,14 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@8.3.6(typescript@5.8.3): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 countries-and-timezones@3.8.0: {} @@ -17129,10 +20943,10 @@ snapshots: create-require@1.1.1: {} - cron@4.3.0: + cron@4.3.3: dependencies: - '@types/luxon': 3.6.2 - luxon: 3.6.1 + '@types/luxon': 3.7.1 + luxon: 3.7.2 cross-spawn@6.0.6: dependencies: @@ -17149,16 +20963,73 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + + css-blank-pseudo@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + css-declaration-sorter@7.3.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-has-pseudo@7.0.3(postcss@8.5.6): + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + css-in-js-utils@3.1.0: dependencies: hyphenate-style-name: 1.1.0 - css-select@5.1.0: + css-loader@6.11.0(webpack@5.104.1): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + semver: 7.7.3 + optionalDependencies: + webpack: 5.104.1 + + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.104.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + cssnano: 6.1.2(postcss@8.5.6) + jest-worker: 29.7.0 + postcss: 8.5.6 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + webpack: 5.104.1 + optionalDependencies: + clean-css: 5.3.3 + + css-prefers-color-scheme@10.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-select@5.2.2: dependencies: boolbase: 1.0.0 - css-what: 6.1.0 + css-what: 6.2.2 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 nth-check: 2.1.1 css-tree@1.1.3: @@ -17176,12 +21047,69 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 - css-what@6.1.0: {} + css-what@6.2.2: {} css.escape@1.5.1: {} + cssdb@8.4.2: {} + cssesc@3.0.0: {} + cssnano-preset-advanced@6.1.2(postcss@8.5.6): + dependencies: + autoprefixer: 10.4.23(postcss@8.5.6) + browserslist: 4.28.1 + cssnano-preset-default: 6.1.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-discard-unused: 6.0.5(postcss@8.5.6) + postcss-merge-idents: 6.0.3(postcss@8.5.6) + postcss-reduce-idents: 6.0.3(postcss@8.5.6) + postcss-zindex: 6.0.2(postcss@8.5.6) + + cssnano-preset-default@6.1.2(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + css-declaration-sorter: 7.3.0(postcss@8.5.6) + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-calc: 9.0.1(postcss@8.5.6) + postcss-colormin: 6.1.0(postcss@8.5.6) + postcss-convert-values: 6.1.0(postcss@8.5.6) + postcss-discard-comments: 6.0.2(postcss@8.5.6) + postcss-discard-duplicates: 6.0.3(postcss@8.5.6) + postcss-discard-empty: 6.0.3(postcss@8.5.6) + postcss-discard-overridden: 6.0.2(postcss@8.5.6) + postcss-merge-longhand: 6.0.5(postcss@8.5.6) + postcss-merge-rules: 6.1.1(postcss@8.5.6) + postcss-minify-font-values: 6.1.0(postcss@8.5.6) + postcss-minify-gradients: 6.0.3(postcss@8.5.6) + postcss-minify-params: 6.1.0(postcss@8.5.6) + postcss-minify-selectors: 6.0.4(postcss@8.5.6) + postcss-normalize-charset: 6.0.2(postcss@8.5.6) + postcss-normalize-display-values: 6.0.2(postcss@8.5.6) + postcss-normalize-positions: 6.0.2(postcss@8.5.6) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.6) + postcss-normalize-string: 6.0.2(postcss@8.5.6) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.6) + postcss-normalize-unicode: 6.1.0(postcss@8.5.6) + postcss-normalize-url: 6.0.2(postcss@8.5.6) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.6) + postcss-ordered-values: 6.0.2(postcss@8.5.6) + postcss-reduce-initial: 6.1.0(postcss@8.5.6) + postcss-reduce-transforms: 6.0.2(postcss@8.5.6) + postcss-svgo: 6.0.3(postcss@8.5.6) + postcss-unique-selectors: 6.0.4(postcss@8.5.6) + + cssnano-utils@4.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + cssnano@6.1.2(postcss@8.5.6): + dependencies: + cssnano-preset-default: 6.1.2(postcss@8.5.6) + lilconfig: 3.1.3 + postcss: 8.5.6 + csso@5.0.5: dependencies: css-tree: 2.2.1 @@ -17191,7 +21119,7 @@ snapshots: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} + csstype@3.2.3: {} d3-array@3.2.4: dependencies: @@ -17260,46 +21188,58 @@ snapshots: dateformat@4.6.3: {} - dayjs@1.11.11: {} + dayjs@1.11.19: {} - dayjs@1.11.13: {} + debounce@1.2.1: {} - dayjs@1.11.18: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 debug@3.2.7: dependencies: ms: 2.1.3 - debug@4.4.1(supports-color@10.1.0): + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 optionalDependencies: - supports-color: 10.1.0 + supports-color: 10.2.2 decimal.js-light@2.5.1: {} decimal.js@10.6.0: {} + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 - dedent@1.6.0(babel-plugin-macros@3.1.0): + dedent@1.7.0(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 - deep-extend@0.6.0: - optional: true + deep-extend@0.6.0: {} deep-is@0.1.4: {} deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defaults@1.0.4: dependencies: clone: 1.0.4 - defaults@3.0.0: {} + defaults@2.0.2: {} defer-to-connect@2.0.1: {} @@ -17309,6 +21249,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -17319,21 +21263,35 @@ snapshots: denque@2.1.0: {} + depd@1.1.2: {} + depd@2.0.0: {} dequal@2.0.3: {} + destroy@1.2.0: {} + detect-indent@6.1.0: optional: true - detect-libc@2.0.4: {} + detect-libc@2.1.2: {} detect-newline@3.1.0: {} detect-node-es@1.1.0: {} - detect-node@2.1.0: - optional: true + detect-node@2.1.0: {} + + detect-port@1.6.1: + dependencies: + address: 1.2.2 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 dezalgo@1.0.4: dependencies: @@ -17344,6 +21302,10 @@ snapshots: diff@4.0.2: {} + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + display-notification@2.0.0: dependencies: escape-string-applescript: 1.0.0 @@ -17352,6 +21314,10 @@ snapshots: dlv@1.1.3: {} + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -17363,17 +21329,20 @@ snapshots: dom-accessibility-api@0.6.3: {} + dom-converter@0.2.0: + dependencies: + utila: 0.4.0 + dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.28.2 - csstype: 3.1.3 + '@babel/runtime': 7.28.4 + csstype: 3.2.3 dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 domhandler: 4.3.1 entities: 2.2.0 - optional: true dom-serializer@2.0.0: dependencies: @@ -17391,7 +21360,6 @@ snapshots: domhandler@4.3.1: dependencies: domelementtype: 2.3.0 - optional: true domhandler@5.0.3: dependencies: @@ -17402,9 +21370,8 @@ snapshots: dom-serializer: 1.4.1 domelementtype: 2.3.0 domhandler: 4.3.1 - optional: true - domutils@3.1.0: + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 @@ -17415,17 +21382,21 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + dotenv-expand@12.0.1: dependencies: - dotenv: 16.5.0 + dotenv: 16.6.1 dotenv@16.0.3: {} dotenv@16.4.7: {} - dotenv@16.5.0: {} + dotenv@16.6.1: {} - dotenv@17.2.2: {} + dotenv@17.2.3: {} dunder-proto@1.0.1: dependencies: @@ -17437,6 +21408,8 @@ snapshots: dependencies: readable-stream: 2.3.8 + duplexer@0.1.2: {} + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -17448,19 +21421,19 @@ snapshots: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.7.2 + semver: 7.7.3 optional: true ee-first@1.1.1: {} ejs@3.1.10: dependencies: - jake: 10.9.2 + jake: 10.9.4 optional: true - electron-to-chromium@1.5.114: {} + electron-to-chromium@1.5.245: {} - electron-to-chromium@1.5.195: {} + electron-to-chromium@1.5.267: {} emitter-listener@1.1.2: dependencies: @@ -17472,31 +21445,35 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} + emojilib@2.4.0: {} - encoding-japanese@2.0.0: - optional: true + emojis-list@3.0.0: {} + + emoticon@4.1.0: {} + + encodeurl@1.0.2: {} - encoding-japanese@2.1.0: + encodeurl@2.0.0: {} + + encoding-japanese@2.2.0: optional: true - end-of-stream@1.4.4: + end-of-stream@1.4.5: dependencies: once: 1.4.0 - enhanced-resolve@5.18.1: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 + tapable: 2.3.0 - entities@2.2.0: - optional: true + entities@2.2.0: {} entities@4.5.0: {} entities@6.0.1: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -17504,60 +21481,6 @@ snapshots: dependencies: stackframe: 1.3.4 - es-abstract@1.23.9: - 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.3 - 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-regex: 1.2.1 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.0 - math-intrinsics: 1.1.0 - object-inspect: 1.13.3 - 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 - 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.18 - es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -17624,7 +21547,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-set-tostringtag: 2.1.0 function-bind: 1.1.2 @@ -17640,6 +21563,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -17651,10 +21576,6 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-shim-unscopables@1.0.2: - dependencies: - hasown: 2.0.2 - es-shim-unscopables@1.1.0: dependencies: hasown: 2.0.2 @@ -17665,67 +21586,90 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.0: + es-toolkit@1.41.0: {} + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.15.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + + esbuild@0.27.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 escalade@3.2.0: {} escape-goat@3.0.0: optional: true + escape-goat@4.0.0: {} + escape-html@1.0.3: {} escape-string-applescript@1.0.0: optional: true + escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.35.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.35.0(jiti@2.4.2)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -17734,9 +21678,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.35.0(jiti@2.4.2) + eslint: 9.39.2(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17748,23 +21692,23 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.35.0(jiti@2.4.2)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@1.21.7)): dependencies: aria-query: 5.3.2 - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.10.3 + axe-core: 4.11.0 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.35.0(jiti@2.4.2) + eslint: 9.39.2(jiti@1.21.7) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -17773,31 +21717,41 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-compiler@19.0.0-beta-af1b7da-20250417(eslint@9.35.0(jiti@2.4.2)): + eslint-plugin-react-compiler@19.0.0-beta-af1b7da-20250417(eslint@9.39.2(jiti@1.21.7)): dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.26.10 - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.28.4) - eslint: 9.35.0(jiti@2.4.2) + '@babel/core': 7.28.6 + '@babel/parser': 7.28.5 + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.28.6) + eslint: 9.39.2(jiti@1.21.7) hermes-parser: 0.25.1 - zod: 3.24.4 - zod-validation-error: 3.4.0(zod@3.24.4) + zod: 3.25.76 + zod-validation-error: 3.5.4(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-hooks@6.1.1(eslint@9.39.2(jiti@1.21.7)): + dependencies: + '@babel/core': 7.28.6 + '@babel/parser': 7.28.5 + eslint: 9.39.2(jiti@1.21.7) + zod: 4.3.5 + zod-validation-error: 4.0.2(zod@4.3.5) transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@5.2.0(eslint@9.35.0(jiti@2.4.2)): + eslint-plugin-react-perf@3.3.3(eslint@9.39.2(jiti@1.21.7)): dependencies: - eslint: 9.35.0(jiti@2.4.2) + eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-react@7.37.5(eslint@9.35.0(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@1.21.7)): dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 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 - eslint: 9.35.0(jiti@2.4.2) + eslint: 9.39.2(jiti@1.21.7) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -17811,11 +21765,11 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-turbo@2.5.6(eslint@9.35.0(jiti@2.4.2))(turbo@2.5.6): + eslint-plugin-turbo@2.7.5(eslint@9.39.2(jiti@1.21.7))(turbo@2.7.5): dependencies: dotenv: 16.0.3 - eslint: 9.35.0(jiti@2.4.2) - turbo: 2.5.6 + eslint: 9.39.2(jiti@1.21.7) + turbo: 2.7.5 eslint-scope@5.1.1: dependencies: @@ -17831,25 +21785,24 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.35.0(jiti@1.21.7): + eslint@9.39.2(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0(jiti@1.21.7)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.1 - '@eslint/core': 0.15.2 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.35.0 - '@eslint/plugin-kit': 0.3.5 - '@humanfs/node': 0.16.6 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 + '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -17873,48 +21826,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint@9.35.0(jiti@2.4.2): - dependencies: - '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0(jiti@2.4.2)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.3.1 - '@eslint/core': 0.15.2 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.35.0 - '@eslint/plugin-kit': 0.3.5 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@10.1.0) - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - 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.4 - optionalDependencies: - jiti: 2.4.2 - transitivePeerDependencies: - - supports-color - esm-env@1.2.2: {} espree@10.4.0: @@ -17943,27 +21854,79 @@ snapshots: estraverse@5.3.0: {} + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-value-to-estree@3.5.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} + eta@2.2.0: {} + etag@1.8.1: {} + eval@0.1.8: + dependencies: + '@types/node': 24.10.8 + require-like: 0.1.2 + eventemitter2@6.4.9: {} eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.1 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} exceljs@4.4.0: dependencies: archiver: 5.3.2 - dayjs: 1.11.11 + dayjs: 1.11.19 fast-csv: 4.3.6 jszip: 3.10.1 readable-stream: 3.6.2 saxes: 5.0.1 - tmp: 0.2.3 + tmp: 0.2.5 unzipper: 0.10.14 uuid: 8.3.2 @@ -17992,33 +21955,61 @@ snapshots: exit-x@0.2.2: {} - expect@30.0.5: + expect@30.2.0: dependencies: - '@jest/expect-utils': 30.0.5 - '@jest/get-type': 30.0.1 - jest-matcher-utils: 30.0.5 - jest-message-util: 30.0.5 - jest-mock: 30.0.5 - jest-util: 30.0.5 + '@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 - expect@30.1.2: + express@4.21.2: dependencies: - '@jest/expect-utils': 30.1.2 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.1.2 - jest-message-util: 30.1.0 - jest-mock: 30.0.5 - jest-util: 30.0.5 + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color - express@5.1.0: + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -18031,12 +22022,12 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.14.1 range-parser: 1.2.1 router: 2.2.0 send: 1.2.0 serve-static: 2.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -18054,13 +22045,13 @@ snapshots: extend-object@1.0.0: optional: true - external-editor@3.1.0: + extend-shallow@2.0.1: dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 + is-extendable: 0.1.1 + + extend@3.0.2: {} - fast-copy@3.0.2: {} + fast-copy@4.0.0: {} fast-csv@4.3.6: dependencies: @@ -18076,8 +22067,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-equals@5.2.2: {} - fast-fifo@1.3.2: {} fast-glob@3.3.1: @@ -18098,13 +22087,13 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-json-stringify@6.0.1: + fast-json-stringify@6.1.1: dependencies: '@fastify/merge-json-schemas': 0.2.1 ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) - fast-uri: 3.0.6 - json-schema-ref-resolver: 2.0.1 + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 fast-levenshtein@2.0.6: {} @@ -18113,13 +22102,11 @@ snapshots: dependencies: fast-decode-uri-component: 1.0.1 - fast-redact@3.5.0: {} - fast-safe-stringify@2.1.1: {} fast-shallow-equal@1.0.0: {} - fast-uri@3.0.6: {} + fast-uri@3.1.0: {} fast-xml-parser@5.2.5: dependencies: @@ -18127,61 +22114,99 @@ snapshots: fastest-stable-stringify@2.0.2: {} - fastify-plugin@5.0.1: {} + fastify-plugin@5.1.0: {} + + fastify@5.6.2: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.1.1 + find-my-way: 9.4.0 + light-my-request: 6.6.0 + pino: 10.2.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.3 + toad-cache: 3.7.0 - fastify@5.4.0: + fastify@5.7.1: dependencies: - '@fastify/ajv-compiler': 4.0.2 - '@fastify/error': 4.1.0 - '@fastify/fast-json-stringify-compiler': 5.0.2 - '@fastify/proxy-addr': 5.0.0 + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 abstract-logging: 2.0.1 avvio: 9.1.0 - fast-json-stringify: 6.0.1 - find-my-way: 9.3.0 + fast-json-stringify: 6.1.1 + find-my-way: 9.4.0 light-my-request: 6.6.0 - pino: 9.7.0 + pino: 10.2.0 process-warning: 5.0.0 rfdc: 1.4.1 - secure-json-parse: 4.0.0 - semver: 7.7.2 + secure-json-parse: 4.1.0 + semver: 7.7.3 toad-cache: 3.7.0 fastq@1.19.1: dependencies: reusify: 1.1.0 + fault@2.0.1: + dependencies: + format: 0.2.2 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + fb-watchman@2.0.2: dependencies: bser: 2.1.1 - fdir@6.4.3(picomatch@4.0.2): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 - fdir@6.4.6(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 + feed@4.2.2: + dependencies: + xml-js: 1.6.11 fflate@0.8.2: {} + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 - file-type@19.6.0: + file-loader@6.2.0(webpack@5.104.1): dependencies: - get-stream: 9.0.1 - strtok3: 9.1.1 - token-types: 6.0.0 - uint8array-extras: 1.4.0 + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.104.1 - file-type@21.0.0: + file-type@20.5.0: dependencies: '@tokenizer/inflate': 0.2.7 - strtok3: 10.2.2 - token-types: 6.0.0 - uint8array-extras: 1.4.0 + strtok3: 10.3.4 + token-types: 6.1.1 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + file-type@21.3.0: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.1 + uint8array-extras: 1.5.0 transitivePeerDependencies: - supports-color @@ -18200,18 +22225,35 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: + finalhandler@1.3.1: dependencies: - debug: 4.4.1(supports-color@10.1.0) + debug: 2.6.9 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.0: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 transitivePeerDependencies: - supports-color - find-my-way@9.3.0: + find-cache-dir@4.0.0: + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + + find-my-way@9.4.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 @@ -18229,15 +22271,20 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + find-versions@5.1.0: dependencies: semver-regex: 4.0.5 fix-dts-default-cjs-exports@1.0.1: dependencies: - magic-string: 0.30.17 - mlly: 1.7.4 - rollup: 4.34.8 + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.52.5 fixpack@4.0.0: dependencies: @@ -18254,13 +22301,11 @@ snapshots: flatted: 3.3.3 keyv: 4.5.4 - flatted@3.3.3: {} + flat@5.0.2: {} - follow-redirects@1.15.9: {} + flatted@3.3.3: {} - for-each@0.3.3: - dependencies: - is-callable: 1.2.7 + follow-redirects@1.15.11: {} for-each@0.3.5: dependencies: @@ -18271,33 +22316,34 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18))): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 chalk: 4.1.2 chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.8.3) + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.2 - tapable: 2.2.1 - typescript: 5.8.3 - webpack: 5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)) + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18)) form-data-encoder@2.1.4: {} - form-data@4.0.2: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -18305,24 +22351,28 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + format@0.2.2: {} + formidable@3.5.4: dependencies: - '@paralleldrive/cuid2': 2.2.2 + '@paralleldrive/cuid2': 2.3.1 dezalgo: 1.0.4 once: 1.4.0 forwarded@0.2.0: {} - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} - framer-motion@12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + framer-motion@12.27.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.23.12 - motion-utils: 12.23.6 + motion-dom: 12.27.1 + motion-utils: 12.24.10 tslib: 2.8.1 optionalDependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + fresh@0.5.2: {} fresh@2.0.0: {} @@ -18331,10 +22381,16 @@ snapshots: fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 universalify: 2.0.1 - fs-monkey@1.0.6: {} + fs-monkey@1.1.0: {} fs.realpath@1.0.0: {} @@ -18368,6 +22424,8 @@ snapshots: dependencies: is-property: 1.0.2 + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -18387,6 +22445,8 @@ snapshots: get-nonce@1.0.1: {} + get-own-enumerable-property-symbols@3.0.2: {} + get-package-type@0.1.0: {} get-port@5.1.1: @@ -18402,17 +22462,14 @@ snapshots: get-stream@6.0.1: {} - get-stream@9.0.1: - dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 + github-slugger@1.5.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -18421,6 +22478,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regex.js@1.2.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + glob-to-regexp@0.4.1: {} glob@10.3.12: @@ -18431,7 +22492,7 @@ snapshots: minipass: 7.1.2 path-scurry: 1.11.1 - glob@10.4.5: + glob@10.5.0: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 @@ -18440,14 +22501,11 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.0.3: + glob@13.0.0: dependencies: - foreground-child: 3.3.1 - jackspeak: 4.1.1 - minimatch: 10.0.3 + minimatch: 10.1.1 minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 2.0.0 + path-scurry: 2.0.1 glob@7.2.3: dependencies: @@ -18458,6 +22516,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + globals@14.0.0: {} globalthis@1.0.4: @@ -18465,8 +22527,39 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 4.0.0 + gopd@1.2.0: {} + got@12.6.1: + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + got@13.0.0: dependencies: '@sindresorhus/is': 5.6.0 @@ -18481,10 +22574,25 @@ snapshots: p-cancelable: 3.0.0 responselike: 3.0.0 + graceful-fs@4.2.10: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + handle-thing@2.0.1: {} + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -18492,14 +22600,12 @@ snapshots: source-map: 0.6.1 wordwrap: 1.0.0 optionalDependencies: - uglify-js: 3.18.0 + uglify-js: 3.19.3 has-bigints@1.1.0: {} has-flag@4.0.0: {} - has-own-prop@2.0.0: {} - has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -18514,12 +22620,107 @@ snapshots: dependencies: has-symbols: 1.1.0 + has-yarn@3.0.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 - he@1.2.0: - optional: true + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.19 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.19 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + he@1.2.0: {} help-me@5.0.0: {} @@ -18529,10 +22730,26 @@ snapshots: dependencies: hermes-estree: 0.25.1 + history@4.10.1: + dependencies: + '@babel/runtime': 7.28.4 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 + hpack.js@2.1.6: + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + hpagent@1.2.0: {} html-encoding-sniffer@4.0.0: @@ -18541,6 +22758,26 @@ snapshots: html-escaper@2.0.2: {} + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.44.1 + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.44.1 + html-minifier@4.0.0: dependencies: camel-case: 3.0.0 @@ -18549,13 +22786,15 @@ snapshots: he: 1.2.0 param-case: 2.1.1 relateurl: 0.2.7 - uglify-js: 3.18.0 + uglify-js: 3.19.3 optional: true html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 + html-tags@3.3.1: {} + html-to-text@9.0.5: dependencies: '@selderee/plugin-htmlparser2': 0.11.0 @@ -18565,6 +22804,18 @@ snapshots: selderee: 0.11.0 optional: true + html-void-elements@3.0.0: {} + + html-webpack-plugin@5.6.4(webpack@5.100.2): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.3.0 + optionalDependencies: + webpack: 5.100.2 + htmlparser2@5.0.1: dependencies: domelementtype: 2.3.0 @@ -18573,23 +22824,38 @@ snapshots: entities: 2.2.0 optional: true + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 entities: 4.5.0 - optional: true htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 entities: 4.5.0 optional: true - http-cache-semantics@4.1.1: {} + http-cache-semantics@4.2.0: {} + + http-deceiver@1.2.7: {} + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 http-errors@2.0.0: dependencies: @@ -18599,36 +22865,60 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-parser-js@0.5.10: {} + http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.3 - debug: 4.4.1(supports-color@10.1.0) + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color + http-proxy-middleware@2.0.9(@types/express@4.17.25): + dependencies: + '@types/http-proxy': 1.17.17 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.25 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.11 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + http2-wrapper@2.2.1: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - https-proxy-agent@7.0.6(supports-color@10.1.0): + https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: - agent-base: 7.1.3 - debug: 4.4.1(supports-color@10.1.0) + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color human-signals@2.1.0: {} + hyperdyperid@1.2.0: {} + hyphenate-style-name@1.1.0: {} i18next-fs-backend@2.6.0: {} - i18next@25.5.2(typescript@5.8.3): + i18next@25.7.4(typescript@5.9.3): dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 iconv-lite@0.4.24: dependencies: @@ -18642,21 +22932,35 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + ieee754@1.2.1: {} ignore@5.3.2: {} - ignore@7.0.4: {} + ignore@7.0.5: {} + + image-size@2.0.2: {} immediate@3.0.6: {} - immer@10.1.3: {} + immer@10.2.0: {} + + immer@11.1.3: {} import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -18666,17 +22970,24 @@ snapshots: indent-string@4.0.0: {} - index-to-position@1.1.0: {} + index-to-position@1.2.0: {} + + infima@0.2.0-alpha.45: {} inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 + inherits@2.0.3: {} + inherits@2.0.4: {} - ini@1.3.8: - optional: true + ini@1.3.8: {} + + ini@2.0.0: {} + + inline-style-parser@0.2.6: {} inline-style-prefixer@7.0.1: dependencies: @@ -18694,10 +23005,21 @@ snapshots: internmap@2.0.3: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} ipaddr.js@2.2.0: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -18706,10 +23028,9 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: {} - - is-async-function@2.1.0: + is-async-function@2.1.1: dependencies: + async-function: 1.0.0 call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 @@ -18723,13 +23044,17 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-boolean-object@1.2.1: + is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 is-callable@1.2.7: {} + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -18745,8 +23070,11 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-docker@2.2.1: - optional: true + is-decimal@2.0.1: {} + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} is-expression@4.0.0: dependencies: @@ -18754,6 +23082,8 @@ snapshots: object-assign: 4.1.1 optional: true + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -18764,9 +23094,10 @@ snapshots: is-generator-fn@2.1.0: {} - is-generator-function@1.1.0: + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 + generator-function: 2.0.1 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -18775,12 +23106,27 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + is-interactive@1.0.0: {} is-map@2.0.3: {} is-negative-zero@2.0.3: {} + is-network-error@1.3.0: {} + + is-npm@6.1.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -18788,8 +23134,22 @@ snapshots: is-number@7.0.0: {} + is-obj@1.0.1: {} + + is-obj@2.0.0: {} + + is-path-inside@3.0.3: {} + is-plain-obj@1.1.0: {} + is-plain-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-potential-custom-element-name@1.0.1: {} is-promise@2.2.2: @@ -18805,11 +23165,13 @@ snapshots: is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 + is-regexp@1.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -18821,11 +23183,9 @@ snapshots: is-stream@2.0.1: {} - is-stream@4.0.1: {} - is-string@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-symbol@1.1.1: @@ -18836,16 +23196,14 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 + + is-typedarray@1.0.0: {} is-unicode-supported@0.1.0: {} is-weakmap@2.0.2: {} - is-weakref@1.1.0: - dependencies: - call-bound: 1.0.4 - is-weakref@1.1.1: dependencies: call-bound: 1.0.4 @@ -18858,7 +23216,14 @@ snapshots: is-wsl@2.2.0: dependencies: is-docker: 2.2.1 - optional: true + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + is-yarn-global@0.4.1: {} + + isarray@0.0.1: {} isarray@1.0.0: {} @@ -18866,15 +23231,17 @@ snapshots: isexe@2.0.0: {} + isobject@3.0.1: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.28.4 - '@babel/parser': 7.28.3 + '@babel/core': 7.28.6 + '@babel/parser': 7.28.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -18886,13 +23253,13 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.29 - debug: 4.4.1(supports-color@10.1.0) + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3(supports-color@10.2.2) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 @@ -18920,43 +23287,38 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.1.1: - dependencies: - '@isaacs/cliui': 8.0.2 - - jake@10.9.2: + jake@10.9.4: dependencies: async: 3.2.6 - chalk: 4.1.2 filelist: 1.0.4 - minimatch: 3.1.2 + picocolors: 1.1.1 optional: true - jest-changed-files@30.0.5: + jest-changed-files@30.2.0: dependencies: execa: 5.1.1 - jest-util: 30.0.5 + jest-util: 30.2.0 p-limit: 3.1.0 - jest-circus@30.1.3(babel-plugin-macros@3.1.0): + jest-circus@30.2.0(babel-plugin-macros@3.1.0): dependencies: - '@jest/environment': 30.1.2 - '@jest/expect': 30.1.2 - '@jest/test-result': 30.1.3 - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@jest/environment': 30.2.0 + '@jest/expect': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 chalk: 4.1.2 co: 4.6.0 - dedent: 1.6.0(babel-plugin-macros@3.1.0) + dedent: 1.7.0(babel-plugin-macros@3.1.0) is-generator-fn: 2.1.0 - jest-each: 30.1.0 - jest-matcher-utils: 30.1.2 - jest-message-util: 30.1.0 - jest-runtime: 30.1.3 - jest-snapshot: 30.1.2 - jest-util: 30.0.5 + 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.0.5 + pretty-format: 30.2.0 pure-rand: 7.0.1 slash: 3.0.0 stack-utils: 2.0.6 @@ -18964,17 +23326,17 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)): + jest-cli@30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)): dependencies: - '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) - '@jest/test-result': 30.1.3 - '@jest/types': 30.0.5 + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) + '@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.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) - jest-util: 30.0.5 - jest-validate: 30.1.0 + jest-config: 30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) + jest-util: 30.2.0 + jest-validate: 30.2.0 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' @@ -18983,308 +23345,298 @@ snapshots: - supports-color - ts-node - jest-config@30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)): + jest-config@30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)): dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 '@jest/get-type': 30.1.0 '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.1.3 - '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.28.4) + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.6) chalk: 4.1.2 - ci-info: 4.3.0 + ci-info: 4.3.1 deepmerge: 4.3.1 - glob: 10.4.5 + glob: 10.5.0 graceful-fs: 4.2.11 - jest-circus: 30.1.3(babel-plugin-macros@3.1.0) - jest-docblock: 30.0.1 - jest-environment-node: 30.1.2 + jest-circus: 30.2.0(babel-plugin-macros@3.1.0) + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 jest-regex-util: 30.0.1 - jest-resolve: 30.1.3 - jest-runner: 30.1.3 - jest-util: 30.0.5 - jest-validate: 30.1.0 + 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.0.5 + pretty-format: 30.2.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.18.1 - ts-node: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3) + '@types/node': 24.10.8 + ts-node: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-diff@30.0.5: - dependencies: - '@jest/diff-sequences': 30.0.1 - '@jest/get-type': 30.0.1 - chalk: 4.1.2 - pretty-format: 30.0.5 - - jest-diff@30.1.2: + jest-diff@30.2.0: dependencies: '@jest/diff-sequences': 30.0.1 '@jest/get-type': 30.1.0 chalk: 4.1.2 - pretty-format: 30.0.5 + pretty-format: 30.2.0 - jest-docblock@30.0.1: + jest-docblock@30.2.0: dependencies: detect-newline: 3.1.0 - jest-each@30.1.0: + jest-each@30.2.0: dependencies: '@jest/get-type': 30.1.0 - '@jest/types': 30.0.5 + '@jest/types': 30.2.0 chalk: 4.1.2 - jest-util: 30.0.5 - pretty-format: 30.0.5 + jest-util: 30.2.0 + pretty-format: 30.2.0 - jest-environment-jsdom@30.1.2: + jest-environment-jsdom@30.2.0: dependencies: - '@jest/environment': 30.1.2 - '@jest/environment-jsdom-abstract': 30.1.2(jsdom@26.1.0) + '@jest/environment': 30.2.0 + '@jest/environment-jsdom-abstract': 30.2.0(jsdom@26.1.0) '@types/jsdom': 21.1.7 - '@types/node': 22.18.1 + '@types/node': 24.10.8 jsdom: 26.1.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - jest-environment-node@30.1.2: + jest-environment-node@30.2.0: dependencies: - '@jest/environment': 30.1.2 - '@jest/fake-timers': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 22.18.1 - jest-mock: 30.0.5 - jest-util: 30.0.5 - jest-validate: 30.1.0 + '@jest/environment': 30.2.0 + '@jest/fake-timers': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 + jest-mock: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 - jest-fixed-jsdom@0.0.10(jest-environment-jsdom@30.1.2): + jest-fixed-jsdom@0.0.11(jest-environment-jsdom@30.2.0): dependencies: - jest-environment-jsdom: 30.1.2 + jest-environment-jsdom: 30.2.0 - jest-haste-map@30.1.0: + jest-haste-map@30.2.0: dependencies: - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 jest-regex-util: 30.0.1 - jest-util: 30.0.5 - jest-worker: 30.1.0 + jest-util: 30.2.0 + jest-worker: 30.2.0 micromatch: 4.0.8 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 - jest-leak-detector@30.1.0: + jest-leak-detector@30.2.0: dependencies: '@jest/get-type': 30.1.0 - pretty-format: 30.0.5 + pretty-format: 30.2.0 - jest-matcher-utils@30.0.5: - dependencies: - '@jest/get-type': 30.0.1 - chalk: 4.1.2 - jest-diff: 30.0.5 - pretty-format: 30.0.5 - - jest-matcher-utils@30.1.2: + jest-matcher-utils@30.2.0: dependencies: '@jest/get-type': 30.1.0 chalk: 4.1.2 - jest-diff: 30.1.2 - pretty-format: 30.0.5 - - jest-message-util@30.0.5: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 30.0.5 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 30.0.5 - slash: 3.0.0 - stack-utils: 2.0.6 + jest-diff: 30.2.0 + pretty-format: 30.2.0 - jest-message-util@30.1.0: + jest-message-util@30.2.0: dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 30.0.5 + '@babel/code-frame': 7.28.6 + '@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.0.5 + pretty-format: 30.2.0 slash: 3.0.0 stack-utils: 2.0.6 - jest-mock@30.0.5: + jest-mock@30.2.0: dependencies: - '@jest/types': 30.0.5 - '@types/node': 22.18.1 - jest-util: 30.0.5 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 + jest-util: 30.2.0 - jest-pnp-resolver@1.2.3(jest-resolve@30.1.3): + jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): optionalDependencies: - jest-resolve: 30.1.3 + jest-resolve: 30.2.0 jest-regex-util@30.0.1: {} - jest-resolve-dependencies@30.1.3: + jest-resolve-dependencies@30.2.0: dependencies: jest-regex-util: 30.0.1 - jest-snapshot: 30.1.2 + jest-snapshot: 30.2.0 transitivePeerDependencies: - supports-color - jest-resolve@30.1.3: + jest-resolve@30.2.0: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 30.1.0 - jest-pnp-resolver: 1.2.3(jest-resolve@30.1.3) - jest-util: 30.0.5 - jest-validate: 30.1.0 + jest-haste-map: 30.2.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.2.0) + jest-util: 30.2.0 + jest-validate: 30.2.0 slash: 3.0.0 unrs-resolver: 1.11.1 - jest-runner@30.1.3: + jest-runner@30.2.0: dependencies: - '@jest/console': 30.1.2 - '@jest/environment': 30.1.2 - '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@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': 24.10.8 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-docblock: 30.0.1 - jest-environment-node: 30.1.2 - jest-haste-map: 30.1.0 - jest-leak-detector: 30.1.0 - jest-message-util: 30.1.0 - jest-resolve: 30.1.3 - jest-runtime: 30.1.3 - jest-util: 30.0.5 - jest-watcher: 30.1.3 - jest-worker: 30.1.0 + 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 transitivePeerDependencies: - supports-color - jest-runtime@30.1.3: + jest-runtime@30.2.0: dependencies: - '@jest/environment': 30.1.2 - '@jest/fake-timers': 30.1.2 - '@jest/globals': 30.1.2 + '@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.1.3 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 chalk: 4.1.2 cjs-module-lexer: 2.1.0 - collect-v8-coverage: 1.0.2 - glob: 10.4.5 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 graceful-fs: 4.2.11 - jest-haste-map: 30.1.0 - jest-message-util: 30.1.0 - jest-mock: 30.0.5 + 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.1.3 - jest-snapshot: 30.1.2 - jest-util: 30.0.5 + jest-resolve: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@30.1.2: + jest-snapshot@30.2.0: dependencies: - '@babel/core': 7.28.4 - '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) - '@babel/types': 7.28.2 - '@jest/expect-utils': 30.1.2 + '@babel/core': 7.28.6 + '@babel/generator': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.6) + '@babel/types': 7.28.5 + '@jest/expect-utils': 30.2.0 '@jest/get-type': 30.1.0 - '@jest/snapshot-utils': 30.1.2 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.4) + '@jest/snapshot-utils': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.6) chalk: 4.1.2 - expect: 30.1.2 + expect: 30.2.0 graceful-fs: 4.2.11 - jest-diff: 30.1.2 - jest-matcher-utils: 30.1.2 - jest-message-util: 30.1.0 - jest-util: 30.0.5 - pretty-format: 30.0.5 - semver: 7.7.2 + 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.3 synckit: 0.11.11 transitivePeerDependencies: - supports-color - jest-util@30.0.5: + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.10.8 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-util@30.2.0: dependencies: - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 chalk: 4.1.2 - ci-info: 4.3.0 + ci-info: 4.3.1 graceful-fs: 4.2.11 - picomatch: 4.0.2 + picomatch: 4.0.3 - jest-validate@30.1.0: + jest-validate@30.2.0: dependencies: '@jest/get-type': 30.1.0 - '@jest/types': 30.0.5 + '@jest/types': 30.2.0 camelcase: 6.3.0 chalk: 4.1.2 leven: 3.1.0 - pretty-format: 30.0.5 + pretty-format: 30.2.0 - jest-watcher@30.1.3: + jest-watcher@30.2.0: dependencies: - '@jest/test-result': 30.1.3 - '@jest/types': 30.0.5 - '@types/node': 22.18.1 + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 24.10.8 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 30.0.5 + jest-util: 30.2.0 string-length: 4.0.2 jest-worker@27.5.1: dependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 24.10.8 + jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest-worker@30.1.0: + jest-worker@30.2.0: dependencies: - '@types/node': 22.18.1 + '@types/node': 24.10.8 '@ungap/structured-clone': 1.3.0 - jest-util: 30.0.5 + jest-util: 30.2.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)): + jest@30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)): dependencies: - '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) - '@jest/types': 30.0.5 + '@jest/core': 30.2.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) + '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + jest-cli: 30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -19294,26 +23646,31 @@ snapshots: jiti@1.21.7: {} - jiti@2.4.2: - optional: true + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 - joi@18.0.1: + joi@18.0.2: dependencies: '@hapi/address': 5.1.1 '@hapi/formula': 3.0.2 '@hapi/hoek': 11.0.7 '@hapi/pinpoint': 2.0.1 - '@hapi/tlds': 1.1.2 + '@hapi/tlds': 1.1.4 '@hapi/topo': 6.0.2 '@standard-schema/spec': 1.0.0 joycon@3.1.1: {} - js-beautify@1.15.1: + js-beautify@1.15.4: dependencies: config-chain: 1.1.13 editorconfig: 1.0.4 - glob: 10.4.5 + glob: 10.5.0 js-cookie: 3.0.5 nopt: 7.2.1 optional: true @@ -19333,14 +23690,14 @@ snapshots: js-toml@1.0.2: dependencies: chevrotain: 11.0.3 - xregexp: 5.1.1 + xregexp: 5.1.2 js-yaml@3.14.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -19351,9 +23708,9 @@ snapshots: decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6(supports-color@10.1.0) + https-proxy-agent: 7.0.6(supports-color@10.2.2) is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.21 + nwsapi: 2.2.22 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -19364,22 +23721,20 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.0 + ws: 8.18.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - jsesc@3.0.2: {} - jsesc@3.1.0: {} json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} - json-schema-ref-resolver@2.0.1: + json-schema-ref-resolver@3.0.0: dependencies: dequal: 2.0.3 @@ -19399,7 +23754,7 @@ snapshots: jsonc-parser@3.3.1: {} - jsonfile@6.1.0: + jsonfile@6.2.0: dependencies: universalify: 2.0.1 optionalDependencies: @@ -19416,7 +23771,20 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.2 + semver: 7.7.3 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 jstransformer@1.0.0: dependencies: @@ -19426,7 +23794,7 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.flat: 1.3.3 object.assign: 4.1.7 object.values: 1.2.1 @@ -19438,7 +23806,7 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - juice@10.0.0: + juice@10.0.1: dependencies: cheerio: 1.0.0-rc.12 commander: 6.2.1 @@ -19449,7 +23817,13 @@ snapshots: - encoding optional: true - jwa@1.4.1: + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 @@ -19457,7 +23831,12 @@ snapshots: jws@3.2.2: dependencies: - jwa: 1.4.1 + jwa: 1.4.2 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 safe-buffer: 5.2.1 jwt-decode@4.0.0: {} @@ -19476,6 +23855,15 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + latest-version@7.0.0: + dependencies: + package-json: 8.1.1 + + launch-editor@2.12.0: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -19490,34 +23878,20 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libbase64@1.2.1: - optional: true - libbase64@1.3.0: optional: true - libmime@5.2.0: - dependencies: - encoding-japanese: 2.0.0 - iconv-lite: 0.6.3 - libbase64: 1.2.1 - libqp: 2.0.1 - optional: true - - libmime@5.3.5: + libmime@5.3.7: dependencies: - encoding-japanese: 2.1.0 + encoding-japanese: 2.2.0 iconv-lite: 0.6.3 libbase64: 1.3.0 - libqp: 2.1.0 + libqp: 2.1.1 optional: true - libphonenumber-js@1.11.4: {} - - libqp@2.0.1: - optional: true + libphonenumber-js@1.12.25: {} - libqp@2.1.0: + libqp@2.1.1: optional: true lie@3.3.0: @@ -19528,7 +23902,7 @@ snapshots: dependencies: cookie: 1.0.2 process-warning: 4.0.1 - set-cookie-parser: 2.7.1 + set-cookie-parser: 2.7.2 lilconfig@3.1.3: {} @@ -19539,27 +23913,33 @@ snapshots: uc.micro: 2.1.0 optional: true - linkify-react@4.3.2(linkifyjs@4.2.0)(react@19.1.1): + linkify-react@4.3.2(linkifyjs@4.3.2)(react@19.2.3): dependencies: - linkifyjs: 4.2.0 - react: 19.1.1 + linkifyjs: 4.3.2 + react: 19.2.3 linkify@0.2.1: {} - linkifyjs@4.2.0: {} + linkifyjs@4.3.2: {} - liquidjs@10.15.0: + liquidjs@10.24.0: dependencies: commander: 10.0.1 optional: true listenercount@1.0.1: {} - load-esm@1.0.2: {} + load-esm@1.0.3: {} load-tsconfig@0.2.5: {} - loader-runner@4.3.0: {} + loader-runner@4.3.1: {} + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 locate-path@5.0.0: dependencies: @@ -19569,6 +23949,10 @@ snapshots: dependencies: p-locate: 5.0.0 + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + lodash-es@4.17.21: {} lodash.debounce@4.0.8: {} @@ -19609,8 +23993,6 @@ snapshots: lodash.once@4.1.1: {} - lodash.sortby@4.7.0: {} - lodash.union@4.6.0: {} lodash.uniq@4.5.0: {} @@ -19622,7 +24004,9 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - long@5.3.1: {} + long@5.3.2: {} + + longest-streak@3.1.0: {} loose-envify@1.4.0: dependencies: @@ -19639,21 +24023,17 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.0.2: {} + lru-cache@11.2.2: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} - lru.min@1.1.2: {} - lucide-react@0.543.0(react@19.1.1): + lucide-react@0.562.0(react@19.2.3): dependencies: - react: 19.1.1 - - luxon@3.6.1: {} + react: 19.2.3 luxon@3.7.2: {} @@ -19663,32 +24043,29 @@ snapshots: magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 - mailparser@3.7.1: + magic-string@0.30.21: dependencies: - encoding-japanese: 2.1.0 + '@jridgewell/sourcemap-codec': 1.5.5 + + mailparser@3.9.0: + dependencies: + '@zone-eu/mailsplit': 5.4.7 + encoding-japanese: 2.2.0 he: 1.2.0 html-to-text: 9.0.5 - iconv-lite: 0.6.3 - libmime: 5.3.5 + iconv-lite: 0.7.0 + libmime: 5.3.7 linkify-it: 5.0.0 - mailsplit: 5.4.0 - nodemailer: 6.9.13 + nodemailer: 7.0.10 punycode.js: 2.3.1 - tlds: 1.252.0 - optional: true - - mailsplit@5.4.0: - dependencies: - libbase64: 1.2.1 - libmime: 5.2.0 - libqp: 2.0.1 + tlds: 1.261.0 optional: true make-dir@4.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 make-error@1.3.6: {} @@ -19696,8 +24073,204 @@ snapshots: dependencies: tmpl: 1.0.5 + markdown-extensions@2.0.0: {} + + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.0.14: {} mdn-data@2.0.28: {} @@ -19710,13 +24283,24 @@ snapshots: memfs@3.5.3: dependencies: - fs-monkey: 1.0.6 + fs-monkey: 1.1.0 + + memfs@4.51.0: + dependencies: + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.5.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 memoize-one@6.0.0: {} mensch@0.3.4: optional: true + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -19725,15 +24309,316 @@ snapshots: methods@1.1.2: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@1.1.0: {} + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@1.1.0: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3(supports-color@10.2.2) + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.33.0: {} + mime-db@1.52.0: {} mime-db@1.54.0: {} + mime-types@2.1.18: + dependencies: + mime-db: 1.33.0 + mime-types@2.1.35: dependencies: mime-db: 1.52.0 @@ -19742,6 +24627,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mime@2.6.0: {} mime@3.0.0: {} @@ -19754,26 +24641,34 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.0.3: + mini-css-extract-plugin@2.9.4(webpack@5.104.1): + dependencies: + schema-utils: 4.3.3 + tapable: 2.3.0 + webpack: 5.104.1 + + minimalistic-assert@1.0.1: {} + + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@5.1.6: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.1: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 optional: true minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -19781,331 +24676,331 @@ snapshots: mitt@3.0.1: {} - mjml-accordion@4.15.3: + mjml-accordion@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-body@4.15.3: + mjml-body@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-button@4.15.3: + mjml-button@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-carousel@4.15.3: + mjml-carousel@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-cli@4.15.3: + mjml-cli@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 chokidar: 3.6.0 - glob: 10.4.5 + glob: 10.5.0 html-minifier: 4.0.0 - js-beautify: 1.15.1 + js-beautify: 1.15.4 lodash: 4.17.21 minimatch: 9.0.5 - mjml-core: 4.15.3 - mjml-migrate: 4.15.3 - mjml-parser-xml: 4.15.3 - mjml-validator: 4.15.3 + mjml-core: 4.16.1 + mjml-migrate: 4.16.1 + mjml-parser-xml: 4.16.1 + mjml-validator: 4.16.1 yargs: 17.7.2 transitivePeerDependencies: - encoding optional: true - mjml-column@4.15.3: + mjml-column@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-core@4.15.3: + mjml-core@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 cheerio: 1.0.0-rc.12 detect-node: 2.1.0 html-minifier: 4.0.0 - js-beautify: 1.15.1 - juice: 10.0.0 + js-beautify: 1.15.4 + juice: 10.0.1 lodash: 4.17.21 - mjml-migrate: 4.15.3 - mjml-parser-xml: 4.15.3 - mjml-validator: 4.15.3 + mjml-migrate: 4.16.1 + mjml-parser-xml: 4.16.1 + mjml-validator: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-divider@4.15.3: + mjml-divider@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-group@4.15.3: + mjml-group@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head-attributes@4.15.3: + mjml-head-attributes@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head-breakpoint@4.15.3: + mjml-head-breakpoint@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head-font@4.15.3: + mjml-head-font@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head-html-attributes@4.15.3: + mjml-head-html-attributes@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head-preview@4.15.3: + mjml-head-preview@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head-style@4.15.3: + mjml-head-style@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head-title@4.15.3: + mjml-head-title@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-head@4.15.3: + mjml-head@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-hero@4.15.3: + mjml-hero@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-image@4.15.3: + mjml-image@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-migrate@4.15.3: + mjml-migrate@4.16.1: dependencies: - '@babel/runtime': 7.28.2 - js-beautify: 1.15.1 + '@babel/runtime': 7.28.4 + js-beautify: 1.15.4 lodash: 4.17.21 - mjml-core: 4.15.3 - mjml-parser-xml: 4.15.3 + mjml-core: 4.16.1 + mjml-parser-xml: 4.16.1 yargs: 17.7.2 transitivePeerDependencies: - encoding optional: true - mjml-navbar@4.15.3: + mjml-navbar@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-parser-xml@4.15.3: + mjml-parser-xml@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 detect-node: 2.1.0 htmlparser2: 9.1.0 lodash: 4.17.21 optional: true - mjml-preset-core@4.15.3: - dependencies: - '@babel/runtime': 7.28.2 - mjml-accordion: 4.15.3 - mjml-body: 4.15.3 - mjml-button: 4.15.3 - mjml-carousel: 4.15.3 - mjml-column: 4.15.3 - mjml-divider: 4.15.3 - mjml-group: 4.15.3 - mjml-head: 4.15.3 - mjml-head-attributes: 4.15.3 - mjml-head-breakpoint: 4.15.3 - mjml-head-font: 4.15.3 - mjml-head-html-attributes: 4.15.3 - mjml-head-preview: 4.15.3 - mjml-head-style: 4.15.3 - mjml-head-title: 4.15.3 - mjml-hero: 4.15.3 - mjml-image: 4.15.3 - mjml-navbar: 4.15.3 - mjml-raw: 4.15.3 - mjml-section: 4.15.3 - mjml-social: 4.15.3 - mjml-spacer: 4.15.3 - mjml-table: 4.15.3 - mjml-text: 4.15.3 - mjml-wrapper: 4.15.3 + mjml-preset-core@4.16.1: + dependencies: + '@babel/runtime': 7.28.4 + mjml-accordion: 4.16.1 + mjml-body: 4.16.1 + mjml-button: 4.16.1 + mjml-carousel: 4.16.1 + mjml-column: 4.16.1 + mjml-divider: 4.16.1 + mjml-group: 4.16.1 + mjml-head: 4.16.1 + mjml-head-attributes: 4.16.1 + mjml-head-breakpoint: 4.16.1 + mjml-head-font: 4.16.1 + mjml-head-html-attributes: 4.16.1 + mjml-head-preview: 4.16.1 + mjml-head-style: 4.16.1 + mjml-head-title: 4.16.1 + mjml-hero: 4.16.1 + mjml-image: 4.16.1 + mjml-navbar: 4.16.1 + mjml-raw: 4.16.1 + mjml-section: 4.16.1 + mjml-social: 4.16.1 + mjml-spacer: 4.16.1 + mjml-table: 4.16.1 + mjml-text: 4.16.1 + mjml-wrapper: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-raw@4.15.3: + mjml-raw@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-section@4.15.3: + mjml-section@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-social@4.15.3: + mjml-social@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-spacer@4.15.3: + mjml-spacer@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-table@4.15.3: + mjml-table@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-text@4.15.3: + mjml-text@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 + mjml-core: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml-validator@4.15.3: + mjml-validator@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 optional: true - mjml-wrapper@4.15.3: + mjml-wrapper@4.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 lodash: 4.17.21 - mjml-core: 4.15.3 - mjml-section: 4.15.3 + mjml-core: 4.16.1 + mjml-section: 4.16.1 transitivePeerDependencies: - encoding optional: true - mjml@4.15.3: + mjml@4.16.1: dependencies: - '@babel/runtime': 7.27.1 - mjml-cli: 4.15.3 - mjml-core: 4.15.3 - mjml-migrate: 4.15.3 - mjml-preset-core: 4.15.3 - mjml-validator: 4.15.3 + '@babel/runtime': 7.28.4 + mjml-cli: 4.16.1 + mjml-core: 4.16.1 + mjml-migrate: 4.16.1 + mjml-preset-core: 4.16.1 + mjml-validator: 4.16.1 transitivePeerDependencies: - encoding optional: true @@ -20114,7 +25009,7 @@ snapshots: dependencies: minimist: 1.2.8 - mlly@1.7.4: + mlly@1.8.0: dependencies: acorn: 8.15.0 pathe: 2.0.3 @@ -20123,11 +25018,15 @@ snapshots: mockdate@3.0.5: {} - motion-dom@12.23.12: + motion-dom@12.27.1: dependencies: - motion-utils: 12.23.6 + motion-utils: 12.24.10 - motion-utils@12.23.6: {} + motion-utils@12.24.10: {} + + mrmime@2.0.1: {} + + ms@2.0.0: {} ms@2.1.3: {} @@ -20141,17 +25040,22 @@ snapshots: type-is: 1.6.18 xtend: 4.0.2 + multicast-dns@7.2.5: + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + mute-stream@2.0.0: {} - mysql2@3.14.5: + mysql2@3.16.1: dependencies: aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 - iconv-lite: 0.7.0 - long: 5.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 lru.min: 1.1.2 - named-placeholders: 1.1.3 + named-placeholders: 1.1.6 seq-queue: 0.0.5 sqlstring: 2.3.3 @@ -20161,95 +25065,102 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - named-placeholders@1.1.3: + named-placeholders@1.1.6: dependencies: - lru-cache: 7.18.3 + lru.min: 1.1.2 - nano-css@5.6.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + nano-css@5.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 css-tree: 1.1.3 - csstype: 3.1.3 + csstype: 3.2.3 fastest-stable-stringify: 2.0.2 inline-style-prefixer: 7.0.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) rtl-css-js: 1.16.1 stacktrace-js: 2.0.2 - stylis: 4.3.4 + stylis: 4.3.6 nanoid@3.3.11: {} - napi-postinstall@0.3.2: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} - nestjs-cls@6.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@6.2.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-pino@4.4.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@10.5.0)(pino@9.9.4)(rxjs@7.8.2): + nestjs-pino@4.5.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.2.0)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - pino: 9.9.4 - pino-http: 10.5.0 + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + pino: 10.2.0 + pino-http: 11.0.0 rxjs: 7.8.2 - nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))): + nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))): dependencies: - '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + typeorm: 0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) - next-i18next@15.4.2(i18next@25.5.2(typescript@5.8.3))(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-i18next@15.7.3(i18next@25.5.2(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3))(react@19.1.1): + next-i18next@15.4.3(@types/react@19.2.8)(i18next@25.7.4(typescript@5.9.3))(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-i18next@16.5.3(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.27.1 - '@types/hoist-non-react-statics': 3.3.6 - core-js: 3.39.0 + '@babel/runtime': 7.28.4 + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.8) + core-js: 3.46.0 hoist-non-react-statics: 3.3.2 - i18next: 25.5.2(typescript@5.8.3) + i18next: 25.7.4(typescript@5.9.3) i18next-fs-backend: 2.6.0 - next: 15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-i18next: 15.7.3(i18next@25.5.2(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3) + next: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-i18next: 16.5.3(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + transitivePeerDependencies: + - '@types/react' - next-router-mock@1.0.2(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + next-router-mock@1.0.5(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: - next: 15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 + next: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 - next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@next/env': 15.4.7 + '@next/env': 16.1.3 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001731 + baseline-browser-mapping: 2.9.7 + caniuse-lite: 1.0.30001760 postcss: 8.4.31 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - styled-jsx: 5.1.6(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.6)(babel-plugin-macros@3.1.0)(react@19.2.3) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.7 - '@next/swc-darwin-x64': 15.4.7 - '@next/swc-linux-arm64-gnu': 15.4.7 - '@next/swc-linux-arm64-musl': 15.4.7 - '@next/swc-linux-x64-gnu': 15.4.7 - '@next/swc-linux-x64-musl': 15.4.7 - '@next/swc-win32-arm64-msvc': 15.4.7 - '@next/swc-win32-x64-msvc': 15.4.7 + '@next/swc-darwin-arm64': 16.1.3 + '@next/swc-darwin-x64': 16.1.3 + '@next/swc-linux-arm64-gnu': 16.1.3 + '@next/swc-linux-arm64-musl': 16.1.3 + '@next/swc-linux-x64-gnu': 16.1.3 + '@next/swc-linux-x64-musl': 16.1.3 + '@next/swc-win32-arm64-msvc': 16.1.3 + '@next/swc-win32-x64-msvc': 16.1.3 '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.55.0 - sharp: 0.34.3 + '@playwright/test': 1.57.0 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -20269,30 +25180,39 @@ snapshots: node-abort-controller@3.1.1: {} - node-addon-api@8.3.1: {} + node-addon-api@8.5.0: {} node-emoji@1.11.0: dependencies: lodash: 4.17.21 + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 optional: true + node-forge@1.3.1: {} + node-gyp-build@4.8.4: {} node-int64@0.4.0: {} - node-releases@2.0.19: {} + node-releases@2.0.27: {} nodemailer@6.10.1: optional: true - nodemailer@6.9.13: + nodemailer@7.0.10: optional: true - nodemailer@7.0.6: {} + nodemailer@7.0.12: {} nopt@7.2.1: dependencies: @@ -20301,9 +25221,7 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - - normalize-url@8.0.1: {} + normalize-url@8.1.0: {} npm-run-path@2.0.2: dependencies: @@ -20314,29 +25232,35 @@ snapshots: dependencies: path-key: 3.1.1 + nprogress@0.2.0: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 + null-loader@4.0.1(webpack@5.104.1): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.104.1 + number-flow@0.5.8: dependencies: esm-env: 1.2.2 - nuqs@2.4.3(next@15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + nuqs@2.4.3(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: mitt: 3.0.1 - react: 19.1.1 + react: 19.2.3 optionalDependencies: - next: 15.4.7(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - nwsapi@2.2.21: {} + nwsapi@2.2.22: {} object-assign@4.1.1: {} object-hash@3.0.0: {} - object-inspect@1.13.3: {} - object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -20344,7 +25268,7 @@ snapshots: object.assign@4.1.7: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 has-symbols: 1.1.0 @@ -20361,14 +25285,14 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 object.groupby@1.0.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 object.values@1.2.1: dependencies: @@ -20377,12 +25301,16 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obuf@1.1.2: {} + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: dependencies: ee-first: 1.1.1 + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -20391,22 +25319,37 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open@10.2.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + open@7.4.2: dependencies: is-docker: 2.2.1 is-wsl: 2.2.0 optional: true - openapi-typescript@7.9.1(typescript@5.8.3): + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openapi-typescript@7.10.1(typescript@5.9.3): dependencies: - '@redocly/openapi-core': 1.34.5(supports-color@10.1.0) + '@redocly/openapi-core': 1.34.5(supports-color@10.2.2) ansi-colors: 4.1.3 change-case: 5.4.4 parse-json: 8.3.0 - supports-color: 10.1.0 - typescript: 5.8.3 + supports-color: 10.2.2 + typescript: 5.9.3 yargs-parser: 21.1.1 + opener@1.5.2: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -20428,37 +25371,33 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - os-tmpdir@1.0.2: {} - own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxc-resolver@11.6.1: - dependencies: - napi-postinstall: 0.3.2 + oxc-resolver@11.13.1: optionalDependencies: - '@oxc-resolver/binding-android-arm-eabi': 11.6.1 - '@oxc-resolver/binding-android-arm64': 11.6.1 - '@oxc-resolver/binding-darwin-arm64': 11.6.1 - '@oxc-resolver/binding-darwin-x64': 11.6.1 - '@oxc-resolver/binding-freebsd-x64': 11.6.1 - '@oxc-resolver/binding-linux-arm-gnueabihf': 11.6.1 - '@oxc-resolver/binding-linux-arm-musleabihf': 11.6.1 - '@oxc-resolver/binding-linux-arm64-gnu': 11.6.1 - '@oxc-resolver/binding-linux-arm64-musl': 11.6.1 - '@oxc-resolver/binding-linux-ppc64-gnu': 11.6.1 - '@oxc-resolver/binding-linux-riscv64-gnu': 11.6.1 - '@oxc-resolver/binding-linux-riscv64-musl': 11.6.1 - '@oxc-resolver/binding-linux-s390x-gnu': 11.6.1 - '@oxc-resolver/binding-linux-x64-gnu': 11.6.1 - '@oxc-resolver/binding-linux-x64-musl': 11.6.1 - '@oxc-resolver/binding-wasm32-wasi': 11.6.1 - '@oxc-resolver/binding-win32-arm64-msvc': 11.6.1 - '@oxc-resolver/binding-win32-ia32-msvc': 11.6.1 - '@oxc-resolver/binding-win32-x64-msvc': 11.6.1 + '@oxc-resolver/binding-android-arm-eabi': 11.13.1 + '@oxc-resolver/binding-android-arm64': 11.13.1 + '@oxc-resolver/binding-darwin-arm64': 11.13.1 + '@oxc-resolver/binding-darwin-x64': 11.13.1 + '@oxc-resolver/binding-freebsd-x64': 11.13.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.13.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.13.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.13.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.13.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.13.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.13.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.13.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.13.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.13.1 + '@oxc-resolver/binding-linux-x64-musl': 11.13.1 + '@oxc-resolver/binding-wasm32-wasi': 11.13.1 + '@oxc-resolver/binding-win32-arm64-msvc': 11.13.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.13.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.13.1 p-cancelable@3.0.0: {} @@ -20467,8 +25406,7 @@ snapshots: p-timeout: 3.2.0 optional: true - p-finally@1.0.0: - optional: true + p-finally@1.0.0: {} p-limit@2.3.0: dependencies: @@ -20478,6 +25416,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -20486,10 +25428,28 @@ snapshots: dependencies: p-limit: 3.1.0 + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@6.2.1: + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.3.0 + retry: 0.13.1 + p-timeout@3.2.0: dependencies: p-finally: 1.0.0 - optional: true p-try@2.2.0: {} @@ -20500,6 +25460,13 @@ snapshots: package-json-from-dist@1.0.1: {} + package-json@8.1.1: + dependencies: + got: 12.6.1 + registry-auth-token: 5.1.0 + registry-url: 6.0.1 + semver: 7.7.3 + pako@1.0.11: {} param-case@2.1.1: @@ -20507,28 +25474,44 @@ snapshots: no-case: 2.3.2 optional: true + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 + '@babel/code-frame': 7.28.6 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 parse-json@8.3.0: dependencies: '@babel/code-frame': 7.27.1 - index-to-position: 1.1.0 + index-to-position: 1.2.0 type-fest: 4.41.0 - parse5-htmlparser2-tree-adapter@7.0.0: + parse-numeric-range@1.3.0: {} + + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 parse5: 7.3.0 - optional: true parse5@7.3.0: dependencies: @@ -20542,6 +25525,11 @@ snapshots: parseurl@1.3.3: {} + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + passport-custom@1.1.1: dependencies: passport-strategy: 1.0.0 @@ -20565,8 +25553,12 @@ snapshots: path-exists@4.0.0: {} + path-exists@5.0.0: {} + path-is-absolute@1.0.1: {} + path-is-inside@1.0.2: {} + path-key@2.0.1: optional: true @@ -20579,12 +25571,20 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-scurry@2.0.0: + path-scurry@2.0.1: dependencies: - lru-cache: 11.0.2 + lru-cache: 11.2.2 minipass: 7.1.2 - path-to-regexp@8.2.0: {} + path-to-regexp@0.1.12: {} + + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + + path-to-regexp@3.3.0: {} + + path-to-regexp@8.3.0: {} path-type@4.0.0: {} @@ -20595,10 +25595,6 @@ snapshots: peberminta@0.9.0: optional: true - peek-readable@5.4.2: {} - - peek-readable@7.0.0: {} - pend@1.2.0: {} picocolors@1.1.1: {} @@ -20607,41 +25603,47 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pify@2.3.0: {} pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 - pino-http@10.5.0: + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-http@11.0.0: dependencies: get-caller-file: 2.0.5 - pino: 9.7.0 + pino: 10.1.0 pino-std-serializers: 7.0.0 process-warning: 5.0.0 - pino-pretty@13.1.1: + pino-pretty@13.1.3: dependencies: colorette: 2.0.20 dateformat: 4.6.3 - fast-copy: 3.0.2 + fast-copy: 4.0.0 fast-safe-stringify: 2.1.1 help-me: 5.0.0 joycon: 3.1.1 minimist: 1.2.8 on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 - pump: 3.0.2 - secure-json-parse: 4.0.0 + pino-abstract-transport: 3.0.0 + pump: 3.0.3 + secure-json-parse: 4.1.0 sonic-boom: 4.2.0 - strip-json-comments: 5.0.2 + strip-json-comments: 5.0.3 pino-std-serializers@7.0.0: {} - pino@9.7.0: + pino@10.1.0: dependencies: + '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 - fast-redact: 3.5.0 on-exit-leak-free: 2.1.2 pino-abstract-transport: 2.0.0 pino-std-serializers: 7.0.0 @@ -20652,94 +25654,491 @@ snapshots: sonic-boom: 4.2.0 thread-stream: 3.1.0 - pino@9.9.4: + pino@10.2.0: dependencies: + '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 - fast-redact: 3.5.0 on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 + pino-abstract-transport: 3.0.0 pino-std-serializers: 7.0.0 process-warning: 5.0.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.5.0 sonic-boom: 4.2.0 - thread-stream: 3.1.0 - - pirates@4.0.6: {} + thread-stream: 4.0.0 pirates@4.0.7: {} - piscina@4.7.0: + piscina@4.9.2: + optionalDependencies: + '@napi-rs/nice': 1.1.1 + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-dir@7.0.0: + dependencies: + find-up: 6.3.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + playwright-core@1.57.0: {} + + playwright@1.57.0: + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + + pluralize@8.0.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-calc@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-clamp@4.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-functional-notation@7.0.12(postcss@8.5.6): + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-color-hex-alpha@10.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-rebeccapurple@10.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-colormin@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-convert-values@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-media@11.0.6(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + postcss-custom-properties@14.0.6(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-selectors@8.0.5(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-dir-pseudo-class@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-discard-comments@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-duplicates@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-empty@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-overridden@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-unused@6.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-double-position-gradients@6.0.4(postcss@8.5.6): + dependencies: + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-focus-visible@10.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-focus-within@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-font-variant@5.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-gap-properties@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-image-set-function@7.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-import@16.1.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-js@5.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-lab-function@7.0.12(postcss@8.5.6): + dependencies: + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): + dependencies: + lilconfig: 3.1.3 optionalDependencies: - '@napi-rs/nice': 1.0.1 + jiti: 1.21.7 + postcss: 8.5.6 + yaml: 2.8.2 + + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1): + dependencies: + cosmiconfig: 8.3.6(typescript@5.9.3) + jiti: 1.21.7 + postcss: 8.5.6 + semver: 7.7.3 + webpack: 5.104.1 + transitivePeerDependencies: + - typescript + + postcss-logical@8.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-merge-idents@6.0.3(postcss@8.5.6): + dependencies: + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-merge-longhand@6.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + stylehacks: 6.1.1(postcss@8.5.6) + + postcss-merge-rules@6.1.1(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@6.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@6.0.3(postcss@8.5.6): + dependencies: + colord: 2.9.3 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-params@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@6.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-modules-values@4.0.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-nesting@13.0.2(postcss@8.5.6): + dependencies: + '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.0) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-nesting@14.0.0(postcss@8.5.6): + dependencies: + '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-normalize-charset@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-normalize-display-values@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 - pkg-dir@4.2.0: + postcss-normalize-url@6.0.2(postcss@8.5.6): dependencies: - find-up: 4.1.0 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 - pkg-types@1.3.1: + postcss-normalize-whitespace@6.0.2(postcss@8.5.6): dependencies: - confbox: 0.1.8 - mlly: 1.7.4 - pathe: 2.0.3 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 - playwright-core@1.55.0: {} + postcss-opacity-percentage@3.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 - playwright@1.55.0: + postcss-ordered-values@6.0.2(postcss@8.5.6): dependencies: - playwright-core: 1.55.0 - optionalDependencies: - fsevents: 2.3.2 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 - pluralize@8.0.0: {} + postcss-overflow-shorthand@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 - possible-typed-array-names@1.0.0: {} + postcss-page-break@3.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 - postcss-import@15.1.0(postcss@8.5.6): + postcss-place@10.0.0(postcss@8.5.6): dependencies: postcss: 8.5.6 postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.10 - postcss-import@16.1.1(postcss@8.5.6): + postcss-preset-env@10.4.0(postcss@8.5.6): + dependencies: + '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.6) + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.6) + '@csstools/postcss-color-function': 4.0.12(postcss@8.5.6) + '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.6) + '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.6) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.6) + '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.6) + '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.6) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.6) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.6) + '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.6) + '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.6) + '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.6) + '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.6) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.6) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.6) + '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.6) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.6) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.6) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.6) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.6) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.6) + '@csstools/postcss-normalize-display-values': 4.0.0(postcss@8.5.6) + '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.6) + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.6) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.6) + '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.6) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.6) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.6) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.6) + '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.6) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6) + autoprefixer: 10.4.23(postcss@8.5.6) + browserslist: 4.28.1 + css-blank-pseudo: 7.0.1(postcss@8.5.6) + css-has-pseudo: 7.0.3(postcss@8.5.6) + css-prefers-color-scheme: 10.0.0(postcss@8.5.6) + cssdb: 8.4.2 + postcss: 8.5.6 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.6) + postcss-clamp: 4.1.0(postcss@8.5.6) + postcss-color-functional-notation: 7.0.12(postcss@8.5.6) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.6) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.6) + postcss-custom-media: 11.0.6(postcss@8.5.6) + postcss-custom-properties: 14.0.6(postcss@8.5.6) + postcss-custom-selectors: 8.0.5(postcss@8.5.6) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.6) + postcss-double-position-gradients: 6.0.4(postcss@8.5.6) + postcss-focus-visible: 10.0.1(postcss@8.5.6) + postcss-focus-within: 9.0.1(postcss@8.5.6) + postcss-font-variant: 5.0.0(postcss@8.5.6) + postcss-gap-properties: 6.0.0(postcss@8.5.6) + postcss-image-set-function: 7.0.0(postcss@8.5.6) + postcss-lab-function: 7.0.12(postcss@8.5.6) + postcss-logical: 8.1.0(postcss@8.5.6) + postcss-nesting: 13.0.2(postcss@8.5.6) + postcss-opacity-percentage: 3.0.0(postcss@8.5.6) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.6) + postcss-page-break: 3.0.4(postcss@8.5.6) + postcss-place: 10.0.0(postcss@8.5.6) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.6) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.6) + postcss-selector-not: 8.0.1(postcss@8.5.6) + + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.6): dependencies: postcss: 8.5.6 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.10 + postcss-selector-parser: 7.1.0 - postcss-js@4.0.1(postcss@8.5.6): + postcss-reduce-idents@6.0.3(postcss@8.5.6): dependencies: - camelcase-css: 2.0.1 postcss: 8.5.6 + postcss-value-parser: 4.2.0 - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)): + postcss-reduce-initial@6.1.0(postcss@8.5.6): dependencies: - lilconfig: 3.1.3 - yaml: 2.7.0 - optionalDependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 postcss: 8.5.6 - ts-node: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3) - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.7.0): + postcss-reduce-transforms@6.0.2(postcss@8.5.6): dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 2.4.2 postcss: 8.5.6 - yaml: 2.7.0 + postcss-value-parser: 4.2.0 - postcss-nested@6.2.0(postcss@8.5.6): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.6): dependencies: postcss: 8.5.6 - postcss-selector-parser: 6.1.2 - postcss-nesting@13.0.2(postcss@8.5.6): + postcss-selector-not@8.0.1(postcss@8.5.6): dependencies: - '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.0) - '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) postcss: 8.5.6 postcss-selector-parser: 7.1.0 @@ -20753,8 +26152,33 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sort-media-queries@5.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + sort-css-media-queries: 2.2.0 + + postcss-svgo@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + svgo: 3.3.2 + + postcss-unique-selectors@6.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + postcss-value-parser@4.2.0: {} + postcss-zindex@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -20769,13 +26193,18 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-tailwindcss@0.6.14(@ianvs/prettier-plugin-sort-imports@4.7.0(prettier@3.5.3))(prettier@3.5.3): + prettier-plugin-tailwindcss@0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.0(prettier@3.6.2))(prettier@3.6.2): dependencies: - prettier: 3.5.3 + prettier: 3.6.2 optionalDependencies: - '@ianvs/prettier-plugin-sort-imports': 4.7.0(prettier@3.5.3) + '@ianvs/prettier-plugin-sort-imports': 4.7.0(prettier@3.6.2) - prettier@3.5.3: {} + prettier@3.6.2: {} + + pretty-error@4.0.0: + dependencies: + lodash: 4.17.21 + renderkid: 3.0.0 pretty-format@27.5.1: dependencies: @@ -20783,19 +26212,21 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - pretty-format@30.0.5: + pretty-format@30.2.0: dependencies: '@jest/schemas': 30.0.5 ansi-styles: 5.2.0 react-is: 18.3.1 - preview-email@3.0.20: + pretty-time@1.1.0: {} + + preview-email@3.1.0: dependencies: ci-info: 3.9.0 display-notification: 2.0.0 fixpack: 4.0.0 get-port: 5.1.1 - mailparser: 3.7.1 + mailparser: 3.9.0 nodemailer: 6.10.1 open: 7.4.2 p-event: 4.2.0 @@ -20804,6 +26235,14 @@ snapshots: uuid: 9.0.1 optional: true + prism-react-renderer@2.4.1(react@19.2.3): + dependencies: + '@types/prismjs': 1.26.5 + clsx: 2.1.1 + react: 19.2.3 + + prismjs@1.30.0: {} + process-nextick-args@2.0.1: {} process-warning@4.0.1: {} @@ -20820,14 +26259,22 @@ snapshots: asap: 2.0.6 optional: true + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - proto-list@1.2.4: - optional: true + property-information@6.5.0: {} + + property-information@7.1.0: {} + + proto-list@1.2.4: {} proxy-addr@2.0.7: dependencies: @@ -20864,7 +26311,7 @@ snapshots: jstransformer: 1.0.0 pug-error: 2.1.0 pug-walk: 2.0.0 - resolve: 1.22.10 + resolve: 1.22.11 optional: true pug-lexer@5.0.1: @@ -20915,9 +26362,9 @@ snapshots: pug-strip-comments: 2.0.0 optional: true - pump@3.0.2: + pump@3.0.3: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 punycode.js@2.3.1: @@ -20925,15 +26372,21 @@ snapshots: punycode@2.3.1: {} + pupa@3.3.0: + dependencies: + escape-goat: 4.0.0 + pure-rand@7.0.1: {} - qs@6.14.0: + qs@6.13.0: dependencies: side-channel: 1.1.0 - queue-microtask@1.2.3: {} + qs@6.14.1: + dependencies: + side-channel: 1.1.0 - queue-tick@1.0.1: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -20943,13 +26396,22 @@ snapshots: dependencies: safe-buffer: 5.2.1 + range-parser@1.2.0: {} + range-parser@1.2.1: {} - raw-body@3.0.0: + raw-body@2.5.2: dependencies: bytes: 3.1.2 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 unpipe: 1.0.0 rc@1.2.8: @@ -20958,35 +26420,37 @@ snapshots: ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 - optional: true - react-content-loader@7.1.1(react@19.1.1): + react-content-loader@7.1.1(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 - react-day-picker@8.10.1(date-fns@4.1.0)(react@19.1.1): + react-day-picker@8.10.1(date-fns@4.1.0)(react@19.2.3): dependencies: date-fns: 4.1.0 - react: 19.1.1 + react: 19.2.3 - react-dom@19.1.1(react@19.1.1): + react-dom@19.2.3(react@19.2.3): dependencies: - react: 19.1.1 - scheduler: 0.26.0 + react: 19.2.3 + scheduler: 0.27.0 + + react-fast-compare@3.2.2: {} - react-hook-form@7.62.0(react@19.1.1): + react-hook-form@7.71.1(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 - react-i18next@15.7.3(i18next@25.5.2(typescript@5.8.3))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.8.3): + react-i18next@16.5.3(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 25.5.2(typescript@5.8.3) - react: 19.1.1 + i18next: 25.7.4(typescript@5.9.3) + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: - react-dom: 19.1.1(react@19.1.1) - typescript: 5.8.3 + react-dom: 19.2.3(react@19.2.3) + typescript: 5.9.3 react-is@16.13.1: {} @@ -20994,73 +26458,114 @@ snapshots: react-is@18.3.1: {} - react-remove-scroll-bar@2.3.8(@types/react@19.1.12)(react@19.1.1): + react-json-view-lite@2.5.0(react@19.2.3): + dependencies: + react: 19.2.3 + + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@19.2.3))(webpack@5.100.2): + dependencies: + '@babel/runtime': 7.28.4 + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.3)' + webpack: 5.100.2 + + react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1): dependencies: - react: 19.1.1 - react-style-singleton: 2.2.3(@types/react@19.1.12)(react@19.1.1) + '@types/use-sync-external-store': 0.0.6 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + redux: 5.0.1 + + react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3): + dependencies: + react: 19.2.3 + react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - react-remove-scroll@2.6.3(@types/react@19.1.12)(react@19.1.1): + react-remove-scroll@2.7.1(@types/react@19.2.8)(react@19.2.3): dependencies: - react: 19.1.1 - react-remove-scroll-bar: 2.3.8(@types/react@19.1.12)(react@19.1.1) - react-style-singleton: 2.2.3(@types/react@19.1.12)(react@19.1.1) + react: 19.2.3 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.8)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.8)(react@19.2.3) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.1.12)(react@19.1.1) - use-sidecar: 1.1.3(@types/react@19.1.12)(react@19.1.1) + use-callback-ref: 1.3.3(@types/react@19.2.8)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.8)(react@19.2.3) optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 + + react-router-config@5.1.1(react-router@5.3.4(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + react: 19.2.3 + react-router: 5.3.4(react@19.2.3) + + react-router-dom@5.3.4(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.3 + react-router: 5.3.4(react@19.2.3) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-router@5.3.4(react@19.2.3): + dependencies: + '@babel/runtime': 7.28.4 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 19.2.3 + react-is: 16.13.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 - react-select@5.10.2(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-select@5.10.2(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@19.1.12)(react@19.1.1) - '@floating-ui/dom': 1.7.0 - '@types/react-transition-group': 4.4.12(@types/react@19.1.12) + '@emotion/react': 11.14.0(@types/react@19.2.8)(react@19.2.3) + '@floating-ui/dom': 1.7.4 + '@types/react-transition-group': 4.4.12(@types/react@19.2.8) memoize-one: 6.0.0 prop-types: 15.8.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - use-isomorphic-layout-effect: 1.2.0(@types/react@19.1.12)(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-transition-group: 4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.8)(react@19.2.3) transitivePeerDependencies: - '@types/react' - supports-color - react-smooth@4.0.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1): - dependencies: - fast-equals: 5.2.2 - prop-types: 15.8.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-transition-group: 4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - - react-style-singleton@2.2.3(@types/react@19.1.12)(react@19.1.1): + react-style-singleton@2.2.3(@types/react@19.2.8)(react@19.2.3): dependencies: get-nonce: 1.0.1 - react: 19.1.1 + react: 19.2.3 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - react-transition-group@4.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - react-universal-interface@0.6.2(react@19.1.1)(tslib@2.8.1): + react-universal-interface@0.6.2(react@19.2.3)(tslib@2.8.1): dependencies: - react: 19.1.1 + react: 19.2.3 tslib: 2.8.1 - react-use@17.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-use@17.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@types/js-cookie': 2.2.7 '@xobotyi/scrollbar-width': 1.9.5 @@ -21068,10 +26573,10 @@ snapshots: fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 js-cookie: 2.2.1 - nano-css: 5.6.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-universal-interface: 0.6.2(react@19.1.1)(tslib@2.8.1) + nano-css: 5.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-universal-interface: 0.6.2(react@19.2.3)(tslib@2.8.1) resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 set-harmonic-interval: 1.0.1 @@ -21079,7 +26584,7 @@ snapshots: ts-easing: 0.2.0 tslib: 2.8.1 - react@19.1.1: {} + react@19.2.3: {} read-cache@1.0.0: dependencies: @@ -21113,49 +26618,87 @@ snapshots: real-require@0.2.0: {} - recharts-scale@0.4.5: - dependencies: - decimal.js-light: 2.5.1 - - recharts@2.15.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + recharts@3.5.0(@types/react@19.2.8)(eslint@9.39.2(jiti@1.21.7))(react-dom@19.2.3(react@19.2.3))(react-is@18.3.1)(react@19.2.3)(redux@5.0.1): dependencies: + '@reduxjs/toolkit': 2.10.1(react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3) clsx: 2.1.1 - eventemitter3: 4.0.7 - lodash: 4.17.21 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + decimal.js-light: 2.5.1 + es-toolkit: 1.41.0 + eslint-plugin-react-perf: 3.3.3(eslint@9.39.2(jiti@1.21.7)) + eventemitter3: 5.0.1 + immer: 10.2.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) react-is: 18.3.1 - react-smooth: 4.0.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - recharts-scale: 0.4.5 + react-redux: 9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1) + reselect: 5.1.1 tiny-invariant: 1.3.3 - victory-vendor: 36.9.2 + use-sync-external-store: 1.6.0(react@19.2.3) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - eslint + - redux + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.8 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 - regenerate-unicode-properties@10.2.0: + regenerate-unicode-properties@10.2.2: dependencies: regenerate: 1.4.2 regenerate@1.4.2: {} - regenerator-runtime@0.14.1: {} - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -21165,23 +26708,119 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - regexpu-core@6.2.0: + regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 - regenerate-unicode-properties: 10.2.0 + regenerate-unicode-properties: 10.2.2 regjsgen: 0.8.0 - regjsparser: 0.12.0 + regjsparser: 0.13.0 unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.2.0 + unicode-match-property-value-ecmascript: 2.2.1 + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 regjsgen@0.8.0: {} - regjsparser@0.12.0: + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + + rehype-raw@7.0.0: dependencies: - jsesc: 3.0.2 + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 - relateurl@0.2.7: - optional: true + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + relateurl@0.2.7: {} + + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-emoji@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + emoticon: 4.1.0 + mdast-util-find-and-replace: 3.0.2 + node-emoji: 2.2.0 + unified: 11.0.5 + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + renderkid@3.0.0: + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 6.0.1 repeat-string@1.6.1: {} @@ -21189,6 +26828,12 @@ snapshots: require-from-string@2.0.2: {} + require-like@0.1.2: {} + + requires-port@1.0.0: {} + + reselect@5.1.1: {} + resize-observer-polyfill@1.5.1: {} resolve-alpn@1.2.1: {} @@ -21201,7 +26846,9 @@ snapshots: resolve-from@5.0.0: {} - resolve@1.22.10: + resolve-pathname@3.0.0: {} + + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 @@ -21224,6 +26871,8 @@ snapshots: ret@0.5.0: {} + retry@0.13.1: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -21232,38 +26881,41 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.34.8: + rollup@4.52.5: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.8 - '@rollup/rollup-android-arm64': 4.34.8 - '@rollup/rollup-darwin-arm64': 4.34.8 - '@rollup/rollup-darwin-x64': 4.34.8 - '@rollup/rollup-freebsd-arm64': 4.34.8 - '@rollup/rollup-freebsd-x64': 4.34.8 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.8 - '@rollup/rollup-linux-arm-musleabihf': 4.34.8 - '@rollup/rollup-linux-arm64-gnu': 4.34.8 - '@rollup/rollup-linux-arm64-musl': 4.34.8 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.8 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.8 - '@rollup/rollup-linux-riscv64-gnu': 4.34.8 - '@rollup/rollup-linux-s390x-gnu': 4.34.8 - '@rollup/rollup-linux-x64-gnu': 4.34.8 - '@rollup/rollup-linux-x64-musl': 4.34.8 - '@rollup/rollup-win32-arm64-msvc': 4.34.8 - '@rollup/rollup-win32-ia32-msvc': 4.34.8 - '@rollup/rollup-win32-x64-msvc': 4.34.8 + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 router@2.2.0: dependencies: - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 transitivePeerDependencies: - supports-color @@ -21271,13 +26923,22 @@ snapshots: rtl-css-js@1.16.1: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.4 + + rtlcss@4.3.0: + dependencies: + escalade: 3.2.0 + picocolors: 1.1.1 + postcss: 8.5.6 + strip-json-comments: 3.1.1 run-applescript@3.2.0: dependencies: execa: 0.10.0 optional: true + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -21309,7 +26970,7 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 @@ -21321,6 +26982,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.4.3: {} + saxes@5.0.1: dependencies: xmlchars: 2.2.0 @@ -21329,7 +26992,9 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.26.0: {} + scheduler@0.27.0: {} + + schema-dts@1.1.5: {} schema-utils@3.3.0: dependencies: @@ -21337,7 +27002,7 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - schema-utils@4.3.2: + schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 ajv: 8.17.1 @@ -21346,9 +27011,16 @@ snapshots: screenfull@5.2.0: {} + search-insights@2.17.3: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + secure-json-parse@2.7.0: {} - secure-json-parse@4.0.0: {} + secure-json-parse@4.1.0: {} seek-bzip@2.0.0: dependencies: @@ -21359,23 +27031,50 @@ snapshots: parseley: 0.12.1 optional: true + select-hose@2.0.0: {} + + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.14 + node-forge: 1.3.1 + + semver-diff@4.0.0: + dependencies: + semver: 7.7.3 + semver-regex@4.0.5: {} semver-truncate@3.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 semver@5.7.2: {} semver@6.3.1: {} - semver@7.6.2: {} + semver@7.7.3: {} - semver@7.7.2: {} + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color send@1.2.0: dependencies: - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -21385,7 +27084,7 @@ snapshots: ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -21395,6 +27094,37 @@ snapshots: dependencies: randombytes: 2.1.0 + serve-handler@6.1.6: + dependencies: + bytes: 3.0.0 + content-disposition: 0.5.2 + mime-types: 2.1.18 + minimatch: 3.1.2 + path-is-inside: 1.0.2 + path-to-regexp: 3.3.0 + range-parser: 1.2.0 + + serve-index@1.9.1: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -21404,7 +27134,7 @@ snapshots: transitivePeerDependencies: - supports-color - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: dependencies: @@ -21432,41 +27162,52 @@ snapshots: setimmediate@1.0.5: {} + setprototypeof@1.1.0: {} + setprototypeof@1.2.0: {} - sha.js@2.4.11: + sha.js@2.4.12: dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shallowequal@1.1.0: {} - sharp@0.34.3: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.3 - '@img/sharp-darwin-x64': 0.34.3 - '@img/sharp-libvips-darwin-arm64': 1.2.0 - '@img/sharp-libvips-darwin-x64': 1.2.0 - '@img/sharp-libvips-linux-arm': 1.2.0 - '@img/sharp-libvips-linux-arm64': 1.2.0 - '@img/sharp-libvips-linux-ppc64': 1.2.0 - '@img/sharp-libvips-linux-s390x': 1.2.0 - '@img/sharp-libvips-linux-x64': 1.2.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 - '@img/sharp-linux-arm': 0.34.3 - '@img/sharp-linux-arm64': 0.34.3 - '@img/sharp-linux-ppc64': 0.34.3 - '@img/sharp-linux-s390x': 0.34.3 - '@img/sharp-linux-x64': 0.34.3 - '@img/sharp-linuxmusl-arm64': 0.34.3 - '@img/sharp-linuxmusl-x64': 0.34.3 - '@img/sharp-wasm32': 0.34.3 - '@img/sharp-win32-arm64': 0.34.3 - '@img/sharp-win32-ia32': 0.34.3 - '@img/sharp-win32-x64': 0.34.3 + '@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@1.2.0: dependencies: @@ -21482,32 +27223,34 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + shimmer@1.2.1: {} side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 side-channel-map@1.0.1: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 side-channel-weakmap@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 side-channel-map: 1.0.1 side-channel@1.1.0: dependencies: es-errors: 1.3.0 - object-inspect: 1.13.3 + object-inspect: 1.13.4 side-channel-list: 1.0.0 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -21516,12 +27259,29 @@ snapshots: signal-exit@4.1.0: {} - simple-swizzle@0.2.2: + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + sitemap@7.1.2: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.3 + + skin-tone@2.0.0: dependencies: - is-arrayish: 0.3.2 + unicode-emoji-modifier-base: 1.0.0 slash@3.0.0: {} + slash@4.0.0: {} + slick@1.12.2: optional: true @@ -21530,14 +27290,22 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + sockjs@0.3.24: + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.4 + sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 - sonner@2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + sort-css-media-queries@2.2.0: {} sort-keys-length@1.0.1: dependencies: @@ -21567,18 +27335,41 @@ snapshots: source-map@0.7.4: {} - source-map@0.8.0-beta.0: + source-map@0.7.6: {} + + space-separated-tokens@2.0.2: {} + + spdy-transport@3.0.0: dependencies: - whatwg-url: 7.1.0 + debug: 4.4.3(supports-color@10.2.2) + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + + spdy@4.0.2: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0 + transitivePeerDependencies: + - supports-color split2@4.2.0: {} sprintf-js@1.0.3: {} - sql-highlight@6.0.0: {} + sql-highlight@6.1.0: {} sqlstring@2.3.3: {} + srcset@4.0.0: {} + stack-chain@1.3.7: {} stack-generator@2.0.10: @@ -21602,8 +27393,14 @@ snapshots: stack-generator: 2.0.10 stacktrace-gps: 3.1.2 + statuses@1.5.0: {} + statuses@2.0.1: {} + statuses@2.0.2: {} + + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -21611,13 +27408,14 @@ snapshots: streamsearch@1.1.0: {} - streamx@2.20.2: + streamx@2.23.0: dependencies: + events-universal: 1.0.1 fast-fifo: 1.3.2 - queue-tick: 1.0.1 - text-decoder: 1.2.1 - optionalDependencies: - bare-events: 2.5.0 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a string-length@4.0.2: dependencies: @@ -21634,20 +27432,20 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -21661,7 +27459,7 @@ snapshots: string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 string.prototype.trim@1.2.10: dependencies: @@ -21669,7 +27467,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -21694,13 +27492,26 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 + + strip-bom-string@1.0.0: {} strip-bom@3.0.0: {} @@ -21720,69 +27531,77 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-json-comments@2.0.1: - optional: true + strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} - strip-json-comments@5.0.2: {} + strip-json-comments@5.0.3: {} strnum@2.1.1: {} - strtok3@10.2.2: + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 - peek-readable: 7.0.0 - strtok3@9.1.1: + style-to-js@1.1.19: dependencies: - '@tokenizer/token': 0.3.0 - peek-readable: 5.4.2 + style-to-object: 1.0.12 + + style-to-object@1.0.12: + dependencies: + inline-style-parser: 0.2.6 - styled-jsx@5.1.6(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react@19.1.1): + styled-jsx@5.1.6(@babel/core@7.28.6)(babel-plugin-macros@3.1.0)(react@19.2.3): dependencies: client-only: 0.0.1 - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.6 babel-plugin-macros: 3.1.0 + stylehacks@6.1.1(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + stylis@4.2.0: {} - stylis@4.3.4: {} + stylis@4.3.6: {} sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 - glob: 10.4.5 + glob: 10.5.0 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.6 + pirates: 4.0.7 ts-interface-checker: 0.1.13 - superagent@10.2.3: + superagent@10.3.0: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.1(supports-color@10.1.0) + debug: 4.4.3(supports-color@10.2.2) fast-safe-stringify: 2.1.1 - form-data: 4.0.4 + form-data: 4.0.5 formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.14.0 + qs: 6.14.1 transitivePeerDependencies: - supports-color - supertest@7.1.4: + supertest@7.2.2: dependencies: + cookie-signature: 1.2.2 methods: 1.1.2 - superagent: 10.2.3 + superagent: 10.3.0 transitivePeerDependencies: - supports-color - supports-color@10.1.0: {} + supports-color@10.2.2: {} supports-color@7.2.0: dependencies: @@ -21800,17 +27619,17 @@ snapshots: dependencies: '@trysound/sax': 0.2.0 commander: 7.2.0 - css-select: 5.1.0 + css-select: 5.2.2 css-tree: 2.3.1 - css-what: 6.1.0 + css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 - swagger-ui-dist@5.21.0: + swagger-ui-dist@5.31.0: dependencies: '@scarf/scarf': 1.4.0 - swiper@11.2.10: {} + swiper@12.0.3: {} symbol-observable@4.0.0: {} @@ -21820,13 +27639,13 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - tailwind-merge@3.3.1: {} + tailwind-merge@3.4.0: {} - tailwind-scrollbar-hide@4.0.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))): + tailwind-scrollbar-hide@4.0.0(tailwindcss@3.4.19(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + tailwindcss: 3.4.19(yaml@2.8.2) - tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)): + tailwindcss@3.4.19(yaml@2.8.2): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -21844,49 +27663,80 @@ snapshots: picocolors: 1.1.1 postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 - resolve: 1.22.10 + resolve: 1.22.11 sucrase: 3.35.0 transitivePeerDependencies: - - ts-node + - tsx + - yaml - tapable@2.2.1: {} + tapable@2.3.0: {} tar-stream@2.2.0: dependencies: bl: 4.1.0 - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 tar-stream@3.1.7: dependencies: - b4a: 1.6.7 + b4a: 1.7.3 fast-fifo: 1.3.2 - streamx: 2.20.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a tdigest@0.1.2: dependencies: bintrees: 1.0.2 - terser-webpack-plugin@5.3.14(@swc/core@1.13.5(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))): + terser-webpack-plugin@5.3.14(webpack@5.100.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.100.2 + + terser-webpack-plugin@5.3.14(webpack@5.104.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.104.1 + + terser-webpack-plugin@5.3.16(@swc/core@1.13.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18))): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.39.0 - webpack: 5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)) + terser: 5.44.1 + webpack: 5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18)) optionalDependencies: - '@swc/core': 1.13.5(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.18) + + terser-webpack-plugin@5.3.16(webpack@5.104.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.104.1 - terser@5.39.0: + terser@5.44.1: dependencies: - '@jridgewell/source-map': 0.3.6 + '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -21897,7 +27747,11 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - text-decoder@1.2.1: {} + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a thenify-all@1.6.0: dependencies: @@ -21907,29 +27761,38 @@ snapshots: dependencies: any-promise: 1.3.0 + thingies@2.5.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + thread-stream@3.1.0: dependencies: real-require: 0.2.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + throttle-debounce@3.0.1: {} through@2.3.8: {} + thunky@1.1.0: {} + tiny-invariant@1.3.3: {} + tiny-warning@1.0.3: {} + tinyexec@0.3.2: {} - tinyglobby@0.2.12: + tinyglobby@0.2.15: dependencies: - fdir: 6.4.3(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 - tinyglobby@0.2.14: - dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + tinypool@1.1.1: {} - tlds@1.252.0: + tlds@1.261.0: optional: true tldts-core@6.1.86: {} @@ -21938,14 +27801,16 @@ snapshots: dependencies: tldts-core: 6.1.86 - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - - tmp@0.2.3: {} + tmp@0.2.5: {} tmpl@1.0.5: {} + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -21959,11 +27824,16 @@ snapshots: token-stream@1.0.0: optional: true - token-types@6.0.0: + token-types@6.1.1: dependencies: + '@borewit/text-codec': 0.1.1 '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + toml@3.0.0: {} + + totalist@3.0.1: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -21971,83 +27841,87 @@ snapshots: tr46@0.0.3: optional: true - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - tr46@5.1.1: dependencies: punycode: 2.3.1 traverse@0.3.9: {} + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} - ts-api-utils@2.1.0(typescript@5.8.3): + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: - typescript: 5.8.3 + typescript: 5.9.3 ts-easing@0.2.0: {} ts-interface-checker@0.1.13: {} - ts-jest@29.4.1(@babel/core@7.28.4)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.4))(jest-util@30.0.5)(jest@30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.1.3(@types/node@22.18.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + jest: 30.2.0(@types/node@24.10.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.7.3 type-fest: 4.41.0 - typescript: 5.8.3 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.28.4 - '@jest/transform': 30.1.2 - '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.28.4) - jest-util: 30.0.5 + '@babel/core': 7.28.6 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.6) + jest-util: 30.2.0 - ts-loader@9.5.4(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18))): dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.3 micromatch: 4.0.8 - semver: 7.7.2 - source-map: 0.7.4 - typescript: 5.8.3 - webpack: 5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)) + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18)) - ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3): + ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.18.1 - acorn: 8.14.1 - acorn-walk: 8.3.3 + '@types/node': 24.10.8 + acorn: 8.15.0 + acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.8.3 + typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.13.5(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.18) ts-toolbelt@9.6.0: {} tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.1 - tapable: 2.2.1 + enhanced-resolve: 5.18.3 + tapable: 2.3.0 tsconfig-paths: 4.2.0 tsconfig-paths@3.15.0: @@ -22063,65 +27937,63 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.6.3: {} - tslib@2.8.1: {} - tsup@8.5.0(@swc/core@1.13.5(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.7.0): + tsup@8.5.1(@swc/core@1.13.5(@swc/helpers@0.5.18))(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2): dependencies: - bundle-require: 5.1.0(esbuild@0.25.0) + bundle-require: 5.1.0(esbuild@0.27.0) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.1(supports-color@10.1.0) - esbuild: 0.25.0 + debug: 4.4.3(supports-color@10.2.2) + esbuild: 0.27.0 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.7.0) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) resolve-from: 5.0.0 - rollup: 4.34.8 - source-map: 0.8.0-beta.0 + rollup: 4.52.5 + source-map: 0.7.6 sucrase: 3.35.0 tinyexec: 0.3.2 - tinyglobby: 0.2.12 + tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - '@swc/core': 1.13.5(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.18) postcss: 8.5.6 - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - turbo-darwin-64@2.5.6: + turbo-darwin-64@2.7.5: optional: true - turbo-darwin-arm64@2.5.6: + turbo-darwin-arm64@2.7.5: optional: true - turbo-linux-64@2.5.6: + turbo-linux-64@2.7.5: optional: true - turbo-linux-arm64@2.5.6: + turbo-linux-arm64@2.7.5: optional: true - turbo-windows-64@2.5.6: + turbo-windows-64@2.7.5: optional: true - turbo-windows-arm64@2.5.6: + turbo-windows-arm64@2.7.5: optional: true - turbo@2.5.6: + turbo@2.7.5: optionalDependencies: - turbo-darwin-64: 2.5.6 - turbo-darwin-arm64: 2.5.6 - turbo-linux-64: 2.5.6 - turbo-linux-arm64: 2.5.6 - turbo-windows-64: 2.5.6 - turbo-windows-arm64: 2.5.6 + turbo-darwin-64: 2.7.5 + turbo-darwin-arm64: 2.7.5 + turbo-linux-64: 2.7.5 + turbo-linux-arm64: 2.7.5 + turbo-windows-64: 2.7.5 + turbo-windows-arm64: 2.7.5 type-check@0.4.0: dependencies: @@ -22133,6 +28005,10 @@ snapshots: type-fest@0.21.3: {} + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + type-fest@4.41.0: {} type-is@1.6.18: @@ -22155,7 +28031,7 @@ snapshots: typed-array-byte-length@1.0.3: dependencies: call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 @@ -22164,7 +28040,7 @@ snapshots: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 @@ -22173,76 +28049,80 @@ snapshots: typed-array-length@1.0.7: dependencies: call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 is-typed-array: 1.1.15 - possible-typed-array-names: 1.0.0 + possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + typedarray@0.0.6: {} - typeorm-naming-strategies@4.1.0(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))): + typeorm-naming-strategies@4.1.0(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))): dependencies: - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + typeorm: 0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) - typeorm-transactional@0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3))): + typeorm-transactional@0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3))): dependencies: - '@types/cls-hooked': 4.3.8 + '@types/cls-hooked': 4.3.9 cls-hooked: 4.2.2 reflect-metadata: 0.2.2 - semver: 7.6.2 - typeorm: 0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)) + semver: 7.7.3 + typeorm: 0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)) - typeorm@0.3.26(babel-plugin-macros@3.1.0)(mysql2@3.14.5)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3)): + typeorm@0.3.28(babel-plugin-macros@3.1.0)(mysql2@3.16.1)(ts-node@10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 - ansis: 3.17.0 + ansis: 4.2.0 app-root-path: 3.1.0 buffer: 6.0.3 - dayjs: 1.11.13 - debug: 4.4.1(supports-color@10.1.0) - dedent: 1.6.0(babel-plugin-macros@3.1.0) - dotenv: 16.5.0 - glob: 10.4.5 + dayjs: 1.11.19 + debug: 4.4.3(supports-color@10.2.2) + dedent: 1.7.0(babel-plugin-macros@3.1.0) + dotenv: 16.6.1 + glob: 10.5.0 reflect-metadata: 0.2.2 - sha.js: 2.4.11 - sql-highlight: 6.0.0 + sha.js: 2.4.12 + sql-highlight: 6.1.0 tslib: 2.8.1 uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: - mysql2: 3.14.5 - ts-node: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.17))(@types/node@22.18.1)(typescript@5.8.3) + mysql2: 3.16.1 + ts-node: 10.9.2(@swc/core@1.13.5(@swc/helpers@0.5.18))(@types/node@24.10.8)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - typescript-eslint@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.35.0(jiti@2.4.2) - typescript: 5.8.3 + '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript@5.8.3: {} + typescript@5.9.3: {} uc.micro@2.1.0: optional: true ufo@1.6.1: {} - uglify-js@3.18.0: + uglify-js@3.19.3: optional: true uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 - uint8array-extras@1.4.0: {} + uint8array-extras@1.5.0: {} unbox-primitive@1.1.0: dependencies: @@ -22256,18 +28136,61 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - undici-types@6.21.0: {} + undici-types@7.16.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} + unicode-emoji-modifier-base@1.0.0: {} + unicode-match-property-ecmascript@2.0.0: dependencies: unicode-canonical-property-names-ecmascript: 2.0.1 - unicode-property-aliases-ecmascript: 2.1.0 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 - unicode-match-property-value-ecmascript@2.2.0: {} + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 - unicode-property-aliases-ecmascript@2.1.0: {} + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 universalify@2.0.1: {} @@ -22275,7 +28198,7 @@ snapshots: unrs-resolver@1.11.1: dependencies: - napi-postinstall: 0.3.2 + napi-postinstall: 0.3.4 optionalDependencies: '@unrs/resolver-binding-android-arm-eabi': 1.11.1 '@unrs/resolver-binding-android-arm64': 1.11.1 @@ -22310,18 +28233,35 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - update-browserslist-db@1.1.3(browserslist@4.24.4): + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: - browserslist: 4.24.4 + browserslist: 4.27.0 escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.1): + update-browserslist-db@1.2.2(browserslist@4.28.1): dependencies: - browserslist: 4.25.1 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 + update-notifier@6.0.2: + dependencies: + boxen: 7.1.1 + chalk: 5.6.2 + configstore: 6.0.0 + has-yarn: 3.0.0 + import-lazy: 4.0.0 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + is-npm: 6.1.0 + is-yarn-global: 0.4.1 + latest-version: 7.0.0 + pupa: 3.3.0 + semver: 7.7.3 + semver-diff: 4.0.0 + xdg-basedir: 5.1.0 + upper-case@1.1.3: optional: true @@ -22331,64 +28271,96 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.1.12)(react@19.1.1): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1): + dependencies: + loader-utils: 2.0.4 + mime-types: 2.1.35 + schema-utils: 3.3.0 + webpack: 5.104.1 + optionalDependencies: + file-loader: 6.2.0(webpack@5.104.1) + + use-callback-ref@1.3.3(@types/react@19.2.8)(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - use-isomorphic-layout-effect@1.2.0(@types/react@19.1.12)(react@19.1.1): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.8)(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - use-sidecar@1.1.3(@types/react@19.1.12)(react@19.1.1): + use-sidecar@1.1.3(@types/react@19.2.8)(react@19.2.3): dependencies: detect-node-es: 1.1.0 - react: 19.1.1 + react: 19.2.3 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.1.12 + '@types/react': 19.2.8 - use-sync-external-store@1.4.0(react@19.1.1): + use-sync-external-store@1.6.0(react@19.2.3): dependencies: - react: 19.1.1 - optional: true + react: 19.2.3 util-deprecate@1.0.2: {} + utila@0.4.0: {} + + utility-types@3.11.0: {} + utils-merge@1.0.1: {} uuid@11.1.0: {} + uuid@13.0.0: {} + uuid@8.3.2: {} - uuid@9.0.1: {} + uuid@9.0.1: + optional: true v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 valid-data-url@3.0.1: optional: true - validator@13.12.0: {} + validator@13.15.20: {} + + value-equal@1.0.1: {} vary@1.1.2: {} - victory-vendor@36.9.2: + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + victory-vendor@37.3.6: dependencies: - '@types/d3-array': 3.2.1 + '@types/d3-array': 3.2.2 '@types/d3-ease': 3.0.2 '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.8 - '@types/d3-shape': 3.1.6 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 '@types/d3-time': 3.0.4 '@types/d3-timer': 3.0.2 d3-array: 3.2.4 @@ -22409,15 +28381,21 @@ snapshots: dependencies: makeerror: 1.0.12 - watchpack@2.4.2: + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + wbuf@1.7.3: + dependencies: + minimalistic-assert: 1.0.1 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 + web-namespaces@2.0.1: {} + web-resource-inliner@6.0.1: dependencies: ansi-colors: 4.1.3 @@ -22433,15 +28411,92 @@ snapshots: webidl-conversions@3.0.1: optional: true - webidl-conversions@4.0.2: {} - webidl-conversions@7.0.0: {} + webpack-bundle-analyzer@4.10.2: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.15.0 + acorn-walk: 8.3.4 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + webpack-dev-middleware@7.4.5(webpack@5.100.2): + dependencies: + colorette: 2.0.20 + memfs: 4.51.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + range-parser: 1.2.1 + schema-utils: 4.3.3 + optionalDependencies: + webpack: 5.100.2 + + webpack-dev-server@5.2.2(webpack@5.100.2): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.25 + '@types/express-serve-static-core': 4.19.7 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.10 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.1 + connect-history-api-fallback: 2.0.0 + express: 4.21.2 + graceful-fs: 4.2.11 + http-proxy-middleware: 2.0.9(@types/express@4.17.25) + ipaddr.js: 2.2.0 + launch-editor: 2.12.0 + open: 10.2.0 + p-retry: 6.2.1 + schema-utils: 4.3.3 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 7.4.5(webpack@5.100.2) + ws: 8.18.3 + optionalDependencies: + webpack: 5.100.2 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-merge@5.10.0: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-merge@6.0.1: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + webpack-node-externals@3.0.0: {} webpack-sources@3.3.3: {} - webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17)): + webpack@5.100.2: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -22451,28 +28506,112 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.24.4 + browserslist: 4.27.0 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(webpack@5.100.2) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpack@5.104.1: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(webpack@5.104.1) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpack@5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18)): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(@swc/core@1.13.5(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.5(@swc/helpers@0.5.17))) - watchpack: 2.4.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(@swc/core@1.13.5(@swc/helpers@0.5.18))(webpack@5.104.1(@swc/core@1.13.5(@swc/helpers@0.5.18))) + watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js + webpackbar@6.0.1(webpack@5.104.1): + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + consola: 3.4.2 + figures: 3.2.0 + markdown-table: 2.0.0 + pretty-time: 1.1.0 + std-env: 3.10.0 + webpack: 5.104.1 + wrap-ansi: 7.0.0 + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -22490,16 +28629,10 @@ snapshots: webidl-conversions: 3.0.1 optional: true - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 - is-boolean-object: 1.2.1 + is-boolean-object: 1.2.2 is-number-object: 1.1.1 is-string: 1.1.1 is-symbol: 1.1.1 @@ -22509,16 +28642,16 @@ snapshots: call-bound: 1.0.4 function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 - is-async-function: 2.1.0 + is-async-function: 2.1.1 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 is-regex: 1.2.1 - is-weakref: 1.1.0 + is-weakref: 1.1.1 isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: @@ -22527,15 +28660,6 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.18: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.3 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 @@ -22559,11 +28683,17 @@ snapshots: dependencies: string-width: 4.2.3 + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + + wildcard@2.0.1: {} + with@7.0.2: dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - assert-never: 1.3.0 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + assert-never: 1.4.0 babel-walk: 3.0.0-canary-5 optional: true @@ -22585,26 +28715,45 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrappy@1.0.2: {} + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + write-file-atomic@5.0.1: dependencies: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.18.0: {} + ws@7.5.10: {} + + ws@8.18.3: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xdg-basedir@5.1.0: {} + + xml-js@1.6.11: + dependencies: + sax: 1.4.3 xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} - xregexp@5.1.1: + xregexp@5.1.2: dependencies: - '@babel/runtime-corejs3': 7.26.0 + '@babel/runtime-corejs3': 7.28.4 xtend@4.0.2: {} @@ -22616,7 +28765,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.7.0: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -22639,7 +28788,9 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors-cjs@2.1.2: {} + yocto-queue@1.2.1: {} + + yoctocolors-cjs@2.1.3: {} zip-stream@4.1.1: dependencies: @@ -22647,17 +28798,23 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 - zod-validation-error@3.4.0(zod@3.24.4): + zod-validation-error@3.5.4(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-validation-error@4.0.2(zod@4.3.5): dependencies: - zod: 3.24.4 + zod: 4.3.5 - zod@3.24.4: {} + zod@3.25.76: {} - zod@4.1.5: {} + zod@4.3.5: {} - zustand@5.0.8(@types/react@19.1.12)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)): + zustand@5.0.10(@types/react@19.2.8)(immer@11.1.3)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): optionalDependencies: - '@types/react': 19.1.12 - immer: 10.1.3 - react: 19.1.1 - use-sync-external-store: 1.4.0(react@19.1.1) + '@types/react': 19.2.8 + immer: 11.1.3 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e2b207f9d..d55b80be7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,7 +4,7 @@ packages: - tooling/* catalog: - eslint: ^9.35.0 + eslint: ^9.39.2 prettier: ^3.5.3 - tailwindcss: ^3.4.17 - typescript: ^5.8.3 + tailwindcss: ^3.4.19 + typescript: ^5.9.3 diff --git a/renovate.json b/renovate.json index 897962166..9fb0830a7 100644 --- a/renovate.json +++ b/renovate.json @@ -17,6 +17,10 @@ { "matchPackageNames": ["@swc/core"], "allowedVersions": "<=1.13.5" + }, + { + "matchPackageNames": ["next"], + "allowedVersions": "<16.0.0" } ], "updateInternalDeps": true, diff --git a/tooling/eslint/package.json b/tooling/eslint/package.json index 26fe8b0f1..169b70fec 100644 --- a/tooling/eslint/package.json +++ b/tooling/eslint/package.json @@ -15,16 +15,16 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@eslint/compat": "^1.3.2", - "@next/eslint-plugin-next": "^15.4.7", + "@eslint/compat": "^2.0.1", + "@next/eslint-plugin-next": "^16.1.3", "@ufb/eslint-plugin-header": "workspace:*", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "beta", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-turbo": "^2.5.6", - "typescript-eslint": "^8.43.0" + "eslint-plugin-react-hooks": "^6.1.1", + "eslint-plugin-turbo": "^2.7.5", + "typescript-eslint": "^8.46.0" }, "devDependencies": { "@ufb/prettier-config": "workspace:*", diff --git a/tooling/github/setup/action.yml b/tooling/github/setup/action.yml index 8b18fe771..a1977142a 100644 --- a/tooling/github/setup/action.yml +++ b/tooling/github/setup/action.yml @@ -5,7 +5,7 @@ runs: using: composite steps: - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/tooling/prettier/package.json b/tooling/prettier/package.json index c8c58cf0c..f839f2dec 100644 --- a/tooling/prettier/package.json +++ b/tooling/prettier/package.json @@ -14,10 +14,10 @@ "dependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "prettier": "catalog:", - "prettier-plugin-tailwindcss": "^0.6.14" + "prettier-plugin-tailwindcss": "^0.7.2" }, "devDependencies": { - "@types/node": "22.18.1", + "@types/node": "24.10.8", "@ufb/tsconfig": "workspace:*", "typescript": "catalog:" }, diff --git a/turbo.json b/turbo.json index 89fb2a2a7..9327be009 100644 --- a/turbo.json +++ b/turbo.json @@ -60,17 +60,19 @@ "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_SENDER", - "SMTP_BASE_URL", "SMTP_TLS", "SMTP_CIPHER_SPEC", "SMTP_OPPORTUNISTIC_TLS", "AUTO_MIGRATION", "MASTER_API_KEY", - "BASE_URL", "SKIP_ENV_VALIDATION", "PORT", + "AUTO_FEEDBACK_DELETION_ENABLED", + "AUTO_FEEDBACK_DELETION_PERIOD_DAYS", + "REFESH_TOKEN_EXPIRED_TIME", "ENABLE_AUTO_FEEDBACK_DELETION", - "AUTO_FEEDBACK_DELETION_PERIOD_DAYS" + "BASE_URL", + "SMTP_BASE_URL" ], "globalPassThroughEnv": ["NODE_ENV", "CI", "npm_lifecycle_event"] }