diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..80f8672 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# https://editorconfig.org/ +# This file helps configure your code editor to match the project's formatting convention +# You might need to install an extension to get support for .editorconfig (see editorconfig docs) + +# Indicate this is the top-most EditorConfig file +root = true + +# Ensure your editor matches Prettier settings +[*] +charset = utf-8 +insert_final_newline = true +end_of_line = lf +indent_style = space +indent_size = 2 +max_line_length = 80 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..77d0af1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# https://git-scm.com/docs/gitattributes +# This file configures how git behaves for this repository + +# * : For all files tracked by git: +# text=auto : Have git guess if it is a text file, if it is +# eol=lf : Ensure it has Unix line endings when checked out +# If need, add overrides below this line (e.g. *.bat text eol=crlf) +* text=auto eol=lf diff --git a/.github/actions/build-container/action.yml b/.github/actions/build-container/action.yml new file mode 100644 index 0000000..adc74f3 --- /dev/null +++ b/.github/actions/build-container/action.yml @@ -0,0 +1,74 @@ +name: "Build Containers" +description: "Builds linux/amd64 and linux/arm64 docker containers using provided config." +inputs: + image_name: + description: "Container image name (e.g., scribear/app)." + required: true + build_context: + description: "Docker build context." + required: true + dockerfile: + description: "Path to the Dockerfile." + required: true + dockerhub_username: + description: "DockerHub username." + required: true + dockerhub_password: + description: "DockerHub password." + required: true +runs: + using: "composite" + steps: + # Generate tags and labels for the Docker image + - name: Generate Container Metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.image_name }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ inputs.dockerhub_username }} + password: ${{ inputs.dockerhub_password }} + + - name: Build and push linux/amd64 + uses: docker/build-push-action@v6 + with: + push: true + context: ${{ inputs.build_context }} + file: ${{ inputs.dockerfile }} + platforms: "linux/amd64" + # Tag image with generated tags and labels + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # Use build cache for faster builds + cache-from: type=registry,ref=${{ inputs.image_name }}:buildcache-linux-amd64 + cache-to: type=registry,ref=${{ inputs.image_name }}:buildcache-linux-amd64,mode=max + + - name: Build and push linux/arm64 + uses: docker/build-push-action@v6 + with: + push: true + context: ${{ inputs.build_context }} + file: ${{ inputs.dockerfile }} + platforms: "linux/arm64" + # Tag image with generated tags and labels + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # Use build cache for faster builds + cache-from: type=registry,ref=${{ inputs.image_name }}:buildcache-linux-arm64 + cache-to: type=registry,ref=${{ inputs.image_name }}:buildcache-linux-arm64,mode=max diff --git a/.github/actions/coverage-report/action.yml b/.github/actions/coverage-report/action.yml new file mode 100644 index 0000000..4792d6c --- /dev/null +++ b/.github/actions/coverage-report/action.yml @@ -0,0 +1,67 @@ +name: "Coverage Report" +description: "Reads provided cobertura coverage report and adds a comment to the PR with the coverage summary." +inputs: + # Inputs (except package_name) are the same as the arguments for irongut/CodeCoverageSummary@v1.3.0 + # see: https://github.com/irongut/CodeCoverageSummary + # All except of packages_name and filename have default values + package_name: + description: "Package name for the coverage report title in PR comments." + required: true + filename: + description: "A cobertura file to load coverage data from." + required: true + badge: + description: "" + default: "true" + fail_below_min: + description: "" + default: "false" + format: + description: "" + default: markdown + hide_branch_rate: + description: "" + default: "false" + hide_complexity: + description: "" + default: "false" + indicators: + description: "" + default: "true" + output: + description: "" + default: both + thresholds: + description: "" + default: "50 75" +runs: + using: "composite" + steps: + - name: Code Coverage Report + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: ${{ inputs.filename }} + badge: ${{ inputs.badge == 'true' }} + fail_below_min: ${{ inputs.fail_below_min == 'true' }} + format: ${{ inputs.format }} + hide_branch_rate: ${{ inputs.hide_branch_rate == 'true' }} + hide_complexity: ${{ inputs.hide_complexity == 'true' }} + indicators: ${{ inputs.indicators == 'true' }} + output: ${{ inputs.output }} + thresholds: ${{ inputs.thresholds }} + + - name: Add Package Name To Comment + shell: bash + run: | + echo "## ${PACKAGE_NAME} Coverage Report" > tmp && cat code-coverage-results.md >> tmp && mv tmp code-coverage-results.md + env: + PACKAGE_NAME: ${{ inputs.package_name }} + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + # Use package_name as unique identifier for the comment it can be recreated if PR is updated + header: ${{ inputs.package_name }} + recreate: true + path: code-coverage-results.md diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml new file mode 100644 index 0000000..8694ee7 --- /dev/null +++ b/.github/actions/setup-node/action.yml @@ -0,0 +1,22 @@ +name: "Setup Node" +description: "Checks out the repo and installs dependencies for node packages." +inputs: + node-version: + description: "Node.js version to use for setup." + default: "24.10.0" +runs: + using: "composite" + + steps: + - name: Set up Git repository + uses: actions/checkout@v4 + + - name: Set up Node.js ${{ inputs.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Install dependencies + working-directory: "." + shell: bash + run: npm ci diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml new file mode 100644 index 0000000..a97a3d3 --- /dev/null +++ b/.github/workflows/node-ci.yml @@ -0,0 +1,103 @@ +name: node-server CI + +on: + push: + paths: + - apps/* + - libs/* + branches: + - main + - staging + tags: + - "v*.*.*" + pull_request: + workflow_dispatch: + +permissions: + actions: read + contents: read + # Needed in order to add comments to PR + pull-requests: write + +jobs: + check-formatting-node: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Setup Node + uses: ./.github/actions/setup-node + + - name: Check formatting + working-directory: "." + run: npm run format + + run-linter-node: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Setup Node + uses: ./.github/actions/setup-node + + - name: Check formatting + working-directory: "." + run: npm run lint + + run-build-node: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Setup Node + uses: ./.github/actions/setup-node + + - name: Run build + working-directory: "." + run: npm run build + + run-tests-node: + needs: + - run-build-node + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Setup Node + uses: ./.github/actions/setup-node + + - name: Run build + working-directory: "." + run: npm run build + + - name: Run tests + working-directory: "." + run: npm run test + + - name: Code Coverage Report - Base Fastify Server + uses: ./.github/actions/coverage-report + with: + package_name: "Base Fastify Server" + filename: "./libs/base-fastify-server/coverage/cobertura-coverage.xml" + + - name: Code Coverage Report - Session Manager + uses: ./.github/actions/coverage-report + with: + package_name: "Session Manager" + filename: "./apps/session-manager/coverage/cobertura-coverage.xml" + + build-session-manager-container: + needs: + - check-formatting-node + - run-linter-node + - run-build-node + - run-tests-node + runs-on: ubuntu-latest + steps: + - name: Set up Git repository + uses: actions/checkout@v4 + + - name: Build and Push Docker Image + uses: ./.github/actions/build-container + with: + image_name: "scribear/session-manager" + build_context: "." + dockerfile: "./apps/session-manager/Dockerfile" + dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub_password: ${{ secrets.DOCKERHUB_PASSWORD }} diff --git a/.github/workflows/node-server-ci.yml b/.github/workflows/node-server-ci.yml deleted file mode 100644 index f67ce92..0000000 --- a/.github/workflows/node-server-ci.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: node-server CI - -on: - push: - paths: - - node-server/** - branches: - - main - - staging - tags: - - "v*.*.*" - pull_request: - workflow_dispatch: - -permissions: - actions: read - contents: read - -env: - NODE_VERSION: 20 - DOCKERHUB_ORG: 'scribear' - -jobs: - build-test-lint-node-server: - runs-on: ubuntu-latest - steps: - - name: Set up Git repository - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install dependencies - working-directory: "node-server" - run: npm ci - - - name: Run build - working-directory: "node-server" - run: npm run build - - - name: Run tests - working-directory: "node-server" - run: npm run test:ci - - - name: Run linter - working-directory: "node-server" - run: npm run lint - - build-container-node-server: - needs: build-test-lint-node-server - runs-on: ubuntu-latest - steps: - - name: Set up Git repository - uses: actions/checkout@v4 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.DOCKERHUB_ORG }}/node-server - tags: | - type=schedule - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: ./node-server - file: ./node-server/Dockerfile - push: true - platforms: "linux/amd64,linux/arm64" - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.DOCKERHUB_ORG }}/node-server:buildcache - cache-to: type=registry,ref=${{ env.DOCKERHUB_ORG }}/node-server:buildcache,mode=max - build-args: | - BRANCH=${{ steps.meta.outputs.version }} - BUILDNUMBER=${{ github.run_number }} - ARG GITSHA1=${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/whisper-service-ci.yml b/.github/workflows/whisper-service-ci.yml deleted file mode 100644 index 648ee17..0000000 --- a/.github/workflows/whisper-service-ci.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: whisper-service CI - -on: - push: - paths: - - whisper-service/** - branches: - - main - - staging - tags: - - "v*.*.*" - pull_request: - workflow_dispatch: - -permissions: - actions: read - contents: read - -env: - PYTHON_VERSION: 3.12 - DOCKERHUB_ORG: 'scribear' - -jobs: - test-lint-whisper-service: - runs-on: ubuntu-latest - steps: - - name: Set up Git repository - uses: actions/checkout@v4 - - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - working-directory: "whisper-service" - run: "pip install -r requirements.txt" - - - name: Run tests - working-directory: "whisper-service" - run: pytest --cov=. - - - name: Run linter - working-directory: "whisper-service" - run: pylint $(git ls-files '*.py') - - build-cpu-container-whisper-service: - needs: test-lint-whisper-service - runs-on: ubuntu-latest - steps: - - name: Set up Git repository - uses: actions/checkout@v4 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.DOCKERHUB_ORG }}/whisper-service-cpu - tags: | - type=schedule - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: ./whisper-service - file: ./whisper-service/Dockerfile_CPU - push: true - platforms: "linux/amd64,linux/arm64" - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cpu:buildcache - cache-to: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cpu:buildcache,mode=max - build-args: | - BRANCH=${{ steps.meta.outputs.version }} - BUILDNUMBER=${{ github.run_number }} - ARG GITSHA1=${{ github.sha }} - - build-cuda-container-whisper-service: - needs: test-lint-whisper-service - runs-on: ubuntu-latest - steps: - - name: Set up Git repository - uses: actions/checkout@v4 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.DOCKERHUB_ORG }}/whisper-service-cuda - tags: | - type=schedule - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: ./whisper-service - file: ./whisper-service/Dockerfile_CUDA - push: true - platforms: "linux/amd64,linux/arm64" - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cuda:buildcache - cache-to: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cuda:buildcache,mode=max - build-args: | - BRANCH=${{ steps.meta.outputs.version }} - BUILDNUMBER=${{ github.run_number }} - ARG GITSHA1=${{ github.sha }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b59d113 --- /dev/null +++ b/.gitignore @@ -0,0 +1,634 @@ +# https://git-scm.com/docs/gitignore +# This file specifies files that should not be commited to git + +### Place other files to ignore here + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Python.gitignore +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Node.gitignore +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite logs files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/Linux.gitignore +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# Metadata left by Dolphin file manager, which comes with KDE Plasma +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# Log files created by default by the nohup command +nohup.out + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride +Icon[ +] + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/VisualStudioCode.gitignore +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets +!*.code-workspace + +# Built Visual Studio Code Extensions +*.vsix + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/JetBrains.gitignore +# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ +.idea/sonarlint.xml # see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119 + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based HTTP Client +.idea/httpRequests +http-client.private.env.json + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# Apifox Helper cache +.idea/.cache/.Apifox_Helper +.idea/ApifoxUploaderProjectSetting.xml + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/NotepadPP.gitignore +# Notepad++ backups # +*.bak + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/Vim.gitignore +# Swap +[._]*.s[a-v][a-z] +# comment out the next line if you don't need vector files +!*.svg +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/Xcode.gitignore +## User settings +xcuserdata/ + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/SublimeText.gitignore +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +### https://github.com/github/gitignore/blob/622c03138bc602b6834aaba64f3061d478c91333/Global/Emacs.gitignore +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + +# undo-tree +*.~undo-tree~ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..084809e --- /dev/null +++ b/.npmrc @@ -0,0 +1,6 @@ +# https://docs.npmjs.com/cli/v9/configuring-npm/npmrc + +# Ensure NPM saves the exact package version in package.json when adding dependencies +# This ensures the exact same dependencies are always installed +# A mechanism for automatic dependencies updates should be used (e.g. dependabot) +save-exact=true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a281e49 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system", + "python-envs.pythonProjects": [] +} diff --git a/README.md b/README.md deleted file mode 100644 index f61aa32..0000000 --- a/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# About - -This repository houses the node server and whisper service backend apps that are used to provide self hosted transcriptions for [ScribeAR](https://scribear.illinois.edu/v/index.html). - -## Whisper Service - -Whisper service receives an audio stream and returns real-time transcriptions. Users can connect to whisper service via websocket either directly using ScribeAR or by using a node server instance that is connected to whisper service. Whisper service is designed to be flexible by supporting multiple speech recognization implementations, allowing to be adapted for various hardware capabilities (including a Raspberry Pi). - -## Node Server - -Node server is a lightweight service that provides transcription broadcasting capabilities to ScribeAR. A central "kiosk" device is used as an audio source while other users can scan a QR code to receive live transcriptions directly on their own devices. Instead of connecting to whisper service, the kiosk connects to node server, which forwards audio to whisper service instead. This is done to allow a lightweight device to be deployed in classrooms, which a more powerful device can run whisper service elsewhere. - -# Getting Started - -* [Deployment](https://github.com/scribear/ScribeAR-NodeServer/wiki/Deployment) - Run node server and/or whisper service -* [Connecting From Frontend](https://github.com/scribear/ScribeAR-NodeServer/wiki/Connecting-From-Frontend) - Connect to node server or whisper service from the ScribeAR frontend -* [Developing Node Server](https://github.com/scribear/ScribeAR-NodeServer/wiki/Developing-Node-Server) - Understand how to develop for node server -* [Developing Whisper Service](https://github.com/scribear/ScribeAR-NodeServer/wiki/Developing-Whisper-Service) - Understand how to develop for whisper service -* [Documentation](https://github.com/scribear/ScribeAR-NodeServer/wiki/Documentation) - Documentation about node server and whisper service \ No newline at end of file diff --git a/apps/session-manager/Dockerfile b/apps/session-manager/Dockerfile new file mode 100644 index 0000000..3b1a039 --- /dev/null +++ b/apps/session-manager/Dockerfile @@ -0,0 +1,79 @@ +# Build in the root directory using: +# docker build . -f ./apps/session-manager/Dockerfile + + +ARG NODE_VERSION=24.10.0 + +FROM node:${NODE_VERSION} AS build-env + +WORKDIR /app + +# Install dependencies first +# So build can use cached dependency layer if only source code changes +COPY package*.json . + +ARG SRC="session-manager" +COPY apps/${SRC}/package*.json apps/${SRC}/ +ARG LIB="base-fastify-server" +COPY libs/${LIB}/package*.json libs/${LIB}/ +ARG LIB="base-schema" +COPY libs/${LIB}/package*.json libs/${LIB}/ +ARG LIB="session-manager-schema" +COPY libs/${LIB}/package*.json libs/${LIB}/ + +RUN npm ci + +# Copy source code and build +COPY tsconfig.base.json . + +ARG SRC="session-manager" +COPY apps/${SRC} apps/${SRC} +ARG LIB="base-fastify-server" +COPY libs/${LIB} libs/${LIB} +ARG LIB="base-schema" +COPY libs/${LIB} libs/${LIB} +ARG LIB="session-manager-schema" +COPY libs/${LIB} libs/${LIB} + +RUN npm run build + +FROM node:${NODE_VERSION}-alpine3.22 + +WORKDIR /app + +# Install dependencies first +# So build can use cached dependency layer if only source code changes +# This time, only include production dependencies to keep image small +COPY package*.json . + +ARG SRC="session-manager" +COPY apps/${SRC}/package*.json apps/${SRC}/ +ARG LIB="base-fastify-server" +COPY libs/${LIB}/package*.json libs/${LIB}/ +ARG LIB="base-schema" +COPY libs/${LIB}/package*.json libs/${LIB}/ +ARG LIB="session-manager-schema" +COPY libs/${LIB}/package*.json libs/${LIB}/ + +RUN npm ci --omit=dev + +# Copy the build artifacts from build-env container +ARG SRC="session-manager" +COPY --from=build-env /app/apps/${SRC}/dist/src/ /app/apps/${SRC}/dist/src/ +ARG LIB="base-fastify-server" +COPY --from=build-env /app/libs/${LIB}/dist/src/ /app/libs/${LIB}/dist/src/ +ARG LIB="base-schema" +COPY --from=build-env /app/libs/${LIB}/dist/src/ /app/libs/${LIB}/dist/src/ +ARG LIB="session-manager-schema" +COPY --from=build-env /app/libs/${LIB}/dist/src/ /app/libs/${LIB}/dist/src/ + +WORKDIR /app/apps/session-manager + +# Ensure server listens on a defined interface and port inside container +# This docker can map this to a real port on the host +ENV HOST=0.0.0.0 +ENV PORT=80 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --start-interval=1s --retries=3 CMD curl -f http://localhost:80/healthcheck || exit 1 + +CMD ["npm", "start"] diff --git a/apps/session-manager/package.json b/apps/session-manager/package.json new file mode 100644 index 0000000..a048dca --- /dev/null +++ b/apps/session-manager/package.json @@ -0,0 +1,27 @@ +{ + "name": "@scribear/session-manager", + "version": "0.0.0", + "description": "", + "author": "scribear", + "main": "dist/src/index.js", + "type": "module", + "scripts": { + "build": "tsc --build", + "start": "node ./dist/src/index.js", + "start:dev": "node ./dist/src/index.js --dev | pino-pretty", + "dev": "nodemon --exec 'npm run build && npm run start:dev' --watch src --watch ../../libs/base-fastify-server/src --watch ../../libs/base-schema/src --watch ../../libs/session-manager-schema/src src/index.ts", + "format": "prettier --check ./src ./tests", + "format:fix": "prettier --write ./src ./tests", + "lint": "eslint ./src ./tests", + "lint:fix": "eslint ./src ./tests --fix", + "test:dev": "vitest --ui --coverage", + "test": "vitest run" + }, + "dependencies": { + "@fastify/awilix": "8.0.0", + "@fastify/swagger": "9.5.2", + "awilix": "12.0.5", + "env-schema": "6.1.0", + "typebox": "1.0.41" + } +} diff --git a/apps/session-manager/src/app_config/app_config.ts b/apps/session-manager/src/app_config/app_config.ts new file mode 100644 index 0000000..28810c8 --- /dev/null +++ b/apps/session-manager/src/app_config/app_config.ts @@ -0,0 +1,52 @@ +import envSchema from 'env-schema'; +import { Type } from 'typebox'; +import type { Static } from 'typebox'; + +import { LogLevel } from '@scribear/base-fastify-server'; + +const CONFIG_SCHEMA = Type.Object({ + LOG_LEVEL: Type.Enum(LogLevel), + PORT: Type.Integer({ minimum: 0, maximum: 65_535 }), + HOST: Type.String(), +}); + +/** + * Class that loads and provides application configuration + */ +class AppConfig { + private _isDevelopment: boolean; + private _logLevel: LogLevel; + private _port: number; + private _host: string; + + get isDevelopment() { + return this._isDevelopment; + } + + get logLevel() { + return this._logLevel; + } + + get port() { + return this._port; + } + + get host() { + return this._host; + } + + constructor(path?: string) { + this._isDevelopment = process.argv.includes('--dev'); + + const env = envSchema>({ + dotenv: path ? { path, quiet: true } : { quiet: true }, + schema: CONFIG_SCHEMA, + }); + + this._logLevel = env.LOG_LEVEL; + this._port = env.PORT; + this._host = env.HOST; + } +} + +export default AppConfig; diff --git a/apps/session-manager/src/index.ts b/apps/session-manager/src/index.ts new file mode 100644 index 0000000..b6289ed --- /dev/null +++ b/apps/session-manager/src/index.ts @@ -0,0 +1,31 @@ +import AppConfig from './app_config/app_config.js'; +import createServer from './server/create_server.js'; + +/** + * Main entrypoint for session manager server + */ +async function main() { + const config = new AppConfig(); + const { logger, fastify } = await createServer(config); + + // Handle uncaught exceptions and rejections + process.on('uncaughtException', (err) => { + logger.fatal({ msg: 'Uncaught exception', err }); + throw err; // terminate on uncaught errors + }); + + process.on('unhandledRejection', (reason) => { + const err = Error('Unhandled rejection', { cause: reason }); + logger.fatal({ msg: 'Unhandled rejection', err }); + throw err; // terminate on uncaught rejection + }); + + try { + await fastify.listen({ port: config.port, host: config.host }); + } catch (err) { + logger.fatal({ msg: 'Failed to start fastify webserver', err }); + throw err; // terminate if failed to start + } +} + +await main(); diff --git a/apps/session-manager/src/server/create_server.ts b/apps/session-manager/src/server/create_server.ts new file mode 100644 index 0000000..9a8f1ce --- /dev/null +++ b/apps/session-manager/src/server/create_server.ts @@ -0,0 +1,37 @@ +import { asValue } from 'awilix'; + +import { createBaseServer } from '@scribear/base-fastify-server'; + +import type AppConfig from '../app_config/app_config.js'; +import registerDependencies from './dependency_injection/register_dependencies.js'; +import calculatorRouter from './features/calculator/calculator.router.js'; +import healthcheckRouter from './features/healthcheck/healthcheck.router.js'; +import swagger from './plugins/swagger.js'; + +/** + * Initializes fastify server amd registers dependencies and routes + * @param config Application config + * @returns Initialized fastify server + */ +async function createServer(config: AppConfig) { + const { logger, dependencyContainer, fastify } = createBaseServer( + config.logLevel, + ); + + // Only include swagger docs if in development mode + if (config.isDevelopment) { + await fastify.register(swagger); + } + + // Register dependencies with container + dependencyContainer.register({ config: asValue(config) }); + registerDependencies(dependencyContainer); + + // Register routes + fastify.register(healthcheckRouter); + fastify.register(calculatorRouter); + + return { logger, fastify }; +} + +export default createServer; diff --git a/apps/session-manager/src/server/dependency_injection/register_dependencies.ts b/apps/session-manager/src/server/dependency_injection/register_dependencies.ts new file mode 100644 index 0000000..115c02d --- /dev/null +++ b/apps/session-manager/src/server/dependency_injection/register_dependencies.ts @@ -0,0 +1,59 @@ +// Need to import so that declare module '@fastify/awilix' below works +import '@fastify/awilix'; +import { type AwilixContainer, Lifetime, asClass } from 'awilix'; +import type { BaseDependencies } from 'libs/base-fastify-server/src/server/types/base_dependencies.js'; + +import type AppConfig from '../../app_config/app_config.js'; +import CalculatorController from '../features/calculator/calculator.controller.js'; +import CalculatorService from '../features/calculator/calculator.service.js'; +import HealthcheckController from '../features/healthcheck/healthcheck.controller.js'; + +/** + * Define types for entities in dependency container + */ +interface AppDependencies extends BaseDependencies { + config: AppConfig; + + // Healthcheck + healthcheckController: HealthcheckController; + + // Calculator + calculatorController: CalculatorController; + calculatorService: CalculatorService; +} + +/** + * Ensure fastify awilix container is typed correctly + * @see https://github.com/fastify/fastify-awilix?tab=readme-ov-file#typescript-usage + */ +declare module '@fastify/awilix' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface Cradle extends AppDependencies {} + + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface RequestCradle extends AppDependencies {} +} + +/** + * Register all controller, service, and repository classes into dependency container + * @param dependencyContainer Container to load dependencies into + */ +function registerDependencies(dependencyContainer: AwilixContainer) { + dependencyContainer.register({ + // Healthcheck + healthcheckController: asClass(HealthcheckController, { + lifetime: Lifetime.SCOPED, + }), + + // Calculator + calculatorController: asClass(CalculatorController, { + lifetime: Lifetime.SCOPED, + }), + calculatorService: asClass(CalculatorService, { + lifetime: Lifetime.SCOPED, + }), + }); +} + +export default registerDependencies; +export type { AppDependencies }; diff --git a/apps/session-manager/src/server/dependency_injection/resolve_handler.ts b/apps/session-manager/src/server/dependency_injection/resolve_handler.ts new file mode 100644 index 0000000..abc1bc2 --- /dev/null +++ b/apps/session-manager/src/server/dependency_injection/resolve_handler.ts @@ -0,0 +1,44 @@ +import type { FastifyReply, FastifyRequest } from 'fastify'; + +import type { AppDependencies } from './register_dependencies.js'; + +/** + * Creates a wrapper function around provided controller route handler + * Wrapper function resolves controller from request dependency container scope + * and passes request/reply to handler + * @param controller Name of controller to resolve + * @param method Name of method on controller to call + * @returns Wrapped controller method + */ +function resolveHandler< + C extends keyof AppDependencies, + M extends keyof AppDependencies[C], +>(controller: C, method: M): AppDependencies[C][M] { + const wrapper = async (req: FastifyRequest, res: FastifyReply) => { + const routeController = req.diScope.resolve( + controller, + ) as AppDependencies[C]; + + // Throw exception if method is not a function + if ( + !(method in routeController) || + typeof routeController[method] !== 'function' + ) { + throw new Error( + `Failed to resolve handler: Property '${String(method)}' on controller '${controller}' is not a function.`, + ); + } + + const handler = routeController[method].bind(routeController) as ( + req: FastifyRequest, + res: FastifyReply, + ) => unknown; + + return await handler(req, res); + }; + + // Cast the type of the wrapper to be the same as the wrapped handler + return wrapper as AppDependencies[C][M]; +} + +export default resolveHandler; diff --git a/apps/session-manager/src/server/features/calculator/calculator.controller.ts b/apps/session-manager/src/server/features/calculator/calculator.controller.ts new file mode 100644 index 0000000..0541ead --- /dev/null +++ b/apps/session-manager/src/server/features/calculator/calculator.controller.ts @@ -0,0 +1,43 @@ +import type { + BaseFastifyReply, + BaseFastifyRequest, +} from '@scribear/base-fastify-server'; +import { + COMPUTE_BINOMIAL_SCHEMA, + type COMPUTE_MONOMIAL_SCHEMA, +} from '@scribear/session-manager-schema'; + +import type { AppDependencies } from '../../dependency_injection/register_dependencies.js'; +import type CalculatorService from './calculator.service.js'; + +class CalculatorController { + private _calculatorService: CalculatorService; + + constructor(calculatorService: AppDependencies['calculatorService']) { + this._calculatorService = calculatorService; + } + + binomial( + req: BaseFastifyRequest, + res: BaseFastifyReply, + ) { + const { a, b, op } = req.body; + + const result = this._calculatorService.binomial(a, b, op); + + res.code(200).send({ result, reqId: req.id }); + } + + monomial( + req: BaseFastifyRequest, + res: BaseFastifyReply, + ) { + const { a, op } = req.body; + + const result = this._calculatorService.monomial(a, op); + + res.code(200).send({ result, reqId: req.id }); + } +} + +export default CalculatorController; diff --git a/apps/session-manager/src/server/features/calculator/calculator.router.ts b/apps/session-manager/src/server/features/calculator/calculator.router.ts new file mode 100644 index 0000000..e1d27f8 --- /dev/null +++ b/apps/session-manager/src/server/features/calculator/calculator.router.ts @@ -0,0 +1,29 @@ +import type { BaseFastifyInstance } from '@scribear/base-fastify-server'; +import { + COMPUTE_BINOMIAL_ROUTE, + COMPUTE_BINOMIAL_SCHEMA, + COMPUTE_MONOMIAL_ROUTE, + COMPUTE_MONOMIAL_SCHEMA, +} from '@scribear/session-manager-schema'; + +import resolveHandler from '../../dependency_injection/resolve_handler.js'; + +/** + * Registers calculator demo routes + * @param fastify Fastify app instance + */ +function calculatorRouter(fastify: BaseFastifyInstance) { + fastify.route({ + ...COMPUTE_BINOMIAL_ROUTE, + schema: COMPUTE_BINOMIAL_SCHEMA, + handler: resolveHandler('calculatorController', 'binomial'), + }); + + fastify.route({ + ...COMPUTE_MONOMIAL_ROUTE, + schema: COMPUTE_MONOMIAL_SCHEMA, + handler: resolveHandler('calculatorController', 'monomial'), + }); +} + +export default calculatorRouter; diff --git a/apps/session-manager/src/server/features/calculator/calculator.service.ts b/apps/session-manager/src/server/features/calculator/calculator.service.ts new file mode 100644 index 0000000..26c4cf1 --- /dev/null +++ b/apps/session-manager/src/server/features/calculator/calculator.service.ts @@ -0,0 +1,35 @@ +import type { BaseLogger } from '@scribear/base-fastify-server'; + +import type { AppDependencies } from '../../dependency_injection/register_dependencies.js'; + +class CalculatorService { + private _log: BaseLogger; + + constructor(logger: AppDependencies['logger']) { + this._log = logger; + } + + binomial(a: number, b: number, op: '+' | '-') { + this._log.info( + `Performing bionomial operation: ${a.toString()} ${op} ${b.toString()}`, + ); + + if (op === '+') { + return a + b; + } else { + return a - b; + } + } + + monomial(a: number, op: 'square' | 'cube') { + this._log.info(`Performing monomial operation: ${op} ${a.toString()}`); + + if (op === 'square') { + return a * a; + } else { + return a * a * a; + } + } +} + +export default CalculatorService; diff --git a/apps/session-manager/src/server/features/healthcheck/healthcheck.controller.ts b/apps/session-manager/src/server/features/healthcheck/healthcheck.controller.ts new file mode 100644 index 0000000..ece0c99 --- /dev/null +++ b/apps/session-manager/src/server/features/healthcheck/healthcheck.controller.ts @@ -0,0 +1,16 @@ +import type { + BaseFastifyReply, + BaseFastifyRequest, +} from '@scribear/base-fastify-server'; +import { HEALTHCHECK_SCHEMA } from '@scribear/session-manager-schema'; + +class HealthcheckController { + healthcheck( + req: BaseFastifyRequest, + res: BaseFastifyReply, + ) { + res.code(200).send({ reqId: req.id }); + } +} + +export default HealthcheckController; diff --git a/apps/session-manager/src/server/features/healthcheck/healthcheck.router.ts b/apps/session-manager/src/server/features/healthcheck/healthcheck.router.ts new file mode 100644 index 0000000..4c373b0 --- /dev/null +++ b/apps/session-manager/src/server/features/healthcheck/healthcheck.router.ts @@ -0,0 +1,21 @@ +import type { BaseFastifyInstance } from '@scribear/base-fastify-server'; +import { + HEALTHCHECK_ROUTE, + HEALTHCHECK_SCHEMA, +} from '@scribear/session-manager-schema'; + +import resolveHandler from '../../dependency_injection/resolve_handler.js'; + +/** + * Registers healthcheck routes + * @param fastify Fastify app instance + */ +function healthcheckRouter(fastify: BaseFastifyInstance) { + fastify.route({ + ...HEALTHCHECK_ROUTE, + schema: HEALTHCHECK_SCHEMA, + handler: resolveHandler('healthcheckController', 'healthcheck'), + }); +} + +export default healthcheckRouter; diff --git a/apps/session-manager/src/server/plugins/swagger.ts b/apps/session-manager/src/server/plugins/swagger.ts new file mode 100644 index 0000000..b18c4d2 --- /dev/null +++ b/apps/session-manager/src/server/plugins/swagger.ts @@ -0,0 +1,34 @@ +import Swagger from '@fastify/swagger'; +import SwaggerUI from '@fastify/swagger-ui'; +import fastifyPlugin from 'fastify-plugin'; + +import type { BaseFastifyInstance } from '@scribear/base-fastify-server'; + +/** + * Registers Swagger and Swagger UI to generate API documentation + */ +export default fastifyPlugin(async (fastify: BaseFastifyInstance) => { + await fastify.register(Swagger, { + openapi: { + openapi: '3.1.0', + info: { + title: 'Session Manager API', + description: 'The Swagger API documentation for Session Manager API.', + version: '0.0.0', + }, + tags: [ + { + name: 'Healthcheck', + description: 'Server health probe endpoint', + }, + { name: 'Calculator', description: 'Server demo endpoints' }, + ], + }, + }); + + await fastify.register(SwaggerUI, { + routePrefix: '/api-docs', + }); + + fastify.log.info('Swagger documentation is available at /api-docs'); +}); diff --git a/apps/session-manager/template.env b/apps/session-manager/template.env new file mode 100644 index 0000000..5a56c7c --- /dev/null +++ b/apps/session-manager/template.env @@ -0,0 +1,6 @@ +#### Log level to use +LOG_LEVEL=info + +#### Host and port server should listen on +HOST=0.0.0.0 +PORT=8000 diff --git a/apps/session-manager/tests/integration/calculator/binomial.test.ts b/apps/session-manager/tests/integration/calculator/binomial.test.ts new file mode 100644 index 0000000..be56c8f --- /dev/null +++ b/apps/session-manager/tests/integration/calculator/binomial.test.ts @@ -0,0 +1,108 @@ +import { beforeEach, describe, expect } from 'vitest'; +import { type MockProxy, mock } from 'vitest-mock-extended'; + +import { + type BaseFastifyInstance, + LogLevel, +} from '@scribear/base-fastify-server'; + +import AppConfig from '../../../src/app_config/app_config.js'; +import createServer from '../../../src/server/create_server.js'; + +describe('Integration Tests - /calculator/binomial', (it) => { + let fastify: BaseFastifyInstance; + let mockConfig: MockProxy; + + beforeEach(async () => { + mockConfig = mock({ + isDevelopment: false, + logLevel: LogLevel.SILENT, + }); + + const server = await createServer(mockConfig); + fastify = server.fastify; + }); + + /** + * Test that server correctly adds two numbers + */ + it('correctly adds two numbers', async () => { + // Arrange + const request = { a: 12, b: 34, op: '+' }; + const result = 46; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/binomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ result }); + }); + + /** + * Test that server correctly subtracts two numbers + */ + it('correctly subtracts two numbers', async () => { + // Arrange + const request = { a: 12, b: 34, op: '-' }; + const result = -22; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/binomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ result }); + }); + + /** + * Test that server rejects invalid operand + */ + it('rejects invalid operand', async () => { + // Arrange + const request = { a: 12, b: 34.5, op: '+' }; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/binomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ + requestErrors: [ + { + key: '/body/b', + }, + ], + }); + }); + + /** + * Test that server rejects invalid operator + */ + it('rejects invalid operator', async () => { + // Arrange + const request = { a: 12, b: 34, op: '*' }; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/binomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(400); + }); +}); diff --git a/apps/session-manager/tests/integration/calculator/monomial.test.ts b/apps/session-manager/tests/integration/calculator/monomial.test.ts new file mode 100644 index 0000000..b88e966 --- /dev/null +++ b/apps/session-manager/tests/integration/calculator/monomial.test.ts @@ -0,0 +1,108 @@ +import { beforeEach, describe, expect } from 'vitest'; +import { type MockProxy, mock } from 'vitest-mock-extended'; + +import { + type BaseFastifyInstance, + LogLevel, +} from '@scribear/base-fastify-server'; + +import AppConfig from '../../../src/app_config/app_config.js'; +import createServer from '../../../src/server/create_server.js'; + +describe('Integration Tests - /calculator/monomial', (it) => { + let fastify: BaseFastifyInstance; + let mockConfig: MockProxy; + + beforeEach(async () => { + mockConfig = mock({ + isDevelopment: false, + logLevel: LogLevel.SILENT, + }); + + const server = await createServer(mockConfig); + fastify = server.fastify; + }); + + /** + * Test that server correctly adds two numbers + */ + it('correctly adds two numbers', async () => { + // Arrange + const request = { a: 12, op: 'square' }; + const result = 144; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/monomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ result }); + }); + + /** + * Test that server correctly subtracts two numbers + */ + it('correctly subtracts two numbers', async () => { + // Arrange + const request = { a: 12, op: 'cube' }; + const result = 1728; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/monomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ result }); + }); + + /** + * Test that server rejects invalid operand + */ + it('rejects invalid operand', async () => { + // Arrange + const request = { a: 12.5, op: 'square' }; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/monomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ + requestErrors: [ + { + key: '/body/a', + }, + ], + }); + }); + + /** + * Test that server rejects invalid operator + */ + it('rejects invalid operator', async () => { + // Arrange + const request = { a: 12, op: 'quad' }; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/calculator/monomial', + body: request, + }); + + // Assert + expect(response.statusCode).toBe(400); + }); +}); diff --git a/apps/session-manager/tests/integration/healthcheck/healthcheck.test.ts b/apps/session-manager/tests/integration/healthcheck/healthcheck.test.ts new file mode 100644 index 0000000..fe533ec --- /dev/null +++ b/apps/session-manager/tests/integration/healthcheck/healthcheck.test.ts @@ -0,0 +1,41 @@ +import { beforeEach, describe, expect } from 'vitest'; +import { type MockProxy, mock } from 'vitest-mock-extended'; + +import { + type BaseFastifyInstance, + LogLevel, +} from '@scribear/base-fastify-server'; + +import AppConfig from '../../../src/app_config/app_config.js'; +import createServer from '../../../src/server/create_server.js'; + +describe('Integration Tests - /healthcheck', (it) => { + let fastify: BaseFastifyInstance; + let mockConfig: MockProxy; + + beforeEach(async () => { + mockConfig = mock({ + isDevelopment: false, + logLevel: LogLevel.SILENT, + }); + + const server = await createServer(mockConfig); + fastify = server.fastify; + }); + + /** + * Test that server responds successfully on /healthcheck endpoint + */ + it('responds with 200 on /healthcheck', async () => { + // Arrange + + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/healthcheck', + }); + + // Assert + expect(response.statusCode).toBe(200); + }); +}); diff --git a/apps/session-manager/tests/integration/swagger/api_docs.test.ts b/apps/session-manager/tests/integration/swagger/api_docs.test.ts new file mode 100644 index 0000000..4bb57b0 --- /dev/null +++ b/apps/session-manager/tests/integration/swagger/api_docs.test.ts @@ -0,0 +1,51 @@ +import { describe, expect } from 'vitest'; +import { mock } from 'vitest-mock-extended'; + +import { LogLevel } from '@scribear/base-fastify-server'; + +import AppConfig from '../../../src/app_config/app_config.js'; +import createServer from '../../../src/server/create_server.js'; + +describe('Integration Tests - /api-docs', (it) => { + /** + * Test that server does not expose swagger /api-docs endpoint when not in development mode + */ + it('returns 404 on /api-docs when not in development mode', async () => { + // Arrange + const mockConfig = mock({ + isDevelopment: false, + logLevel: LogLevel.SILENT, + }); + const { fastify } = await createServer(mockConfig); + + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/api-docs', + }); + + // Assert + expect(response.statusCode).toBe(404); + }); + + /** + * Test that server does expose swagger /api-docs endpoint when in development mode + */ + it('returns 200 on /api-docs when in development mode', async () => { + // Arrange + const mockConfig = mock({ + isDevelopment: true, + logLevel: LogLevel.SILENT, + }); + const { fastify } = await createServer(mockConfig); + + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/api-docs', + }); + + // Assert + expect(response.statusCode).toBe(200); + }); +}); diff --git a/apps/session-manager/tests/unit/server/features/calculator/calculator.controller.test.ts b/apps/session-manager/tests/unit/server/features/calculator/calculator.controller.test.ts new file mode 100644 index 0000000..d4898e0 --- /dev/null +++ b/apps/session-manager/tests/unit/server/features/calculator/calculator.controller.test.ts @@ -0,0 +1,118 @@ +import { type Mock, beforeEach, describe, expect, vi } from 'vitest'; +import { type MockProxy, mock } from 'vitest-mock-extended'; + +import type { + BaseFastifyReply, + BaseFastifyRequest, +} from '@scribear/base-fastify-server'; +import type { + COMPUTE_BINOMIAL_SCHEMA, + COMPUTE_MONOMIAL_SCHEMA, +} from '@scribear/session-manager-schema'; + +import CalculatorController from '../../../../../src/server/features/calculator/calculator.controller.js'; +import type CalculatorService from '../../../../../src/server/features/calculator/calculator.service.js'; + +describe('Calculator controller', () => { + const testRequestId = 'TEST_REQUEST_ID'; + let mockReply: { + code: Mock; + send: Mock; + }; + let mockCalculatorService: MockProxy; + let calculatorController: CalculatorController; + + beforeEach(() => { + mockReply = { + code: vi.fn().mockReturnThis(), + send: vi.fn(), + }; + + mockCalculatorService = mock(); + calculatorController = new CalculatorController(mockCalculatorService); + }); + + describe('binomial handler', (it) => { + /** + * Test that binomial handler correctly calls calculatorService and replies with result + */ + it('calls calculator service correctly and replies with result', () => { + // Arrange + const result = 46; + mockCalculatorService.binomial.mockReturnValue(result); + const mockReq = { + id: testRequestId, + body: { + a: 12, + b: 34, + op: '+', + }, + }; + + // Act + calculatorController.binomial( + mockReq as unknown as BaseFastifyRequest< + typeof COMPUTE_BINOMIAL_SCHEMA + >, + mockReply as unknown as BaseFastifyReply< + typeof COMPUTE_BINOMIAL_SCHEMA + >, + ); + + // Assert + // ignore linter error caused by vitest-mock-extended + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockCalculatorService.binomial).toHaveBeenCalledExactlyOnceWith( + mockReq.body.a, + mockReq.body.b, + mockReq.body.op, + ); + expect(mockReply.code).toHaveBeenCalledExactlyOnceWith(200); + expect(mockReply.send).toHaveBeenCalledExactlyOnceWith({ + result, + reqId: testRequestId, + }); + }); + }); + + describe('monomial handler', (it) => { + /** + * Test that monomial handler correctly calls calculatorService and replies with result + */ + it('calls calculator service corrently and replies with result', () => { + // Arrange + const result = 144; + mockCalculatorService.monomial.mockReturnValue(result); + const mockReq = { + id: testRequestId, + body: { + a: 12, + op: 'square', + }, + }; + + // Act + calculatorController.monomial( + mockReq as unknown as BaseFastifyRequest< + typeof COMPUTE_MONOMIAL_SCHEMA + >, + mockReply as unknown as BaseFastifyReply< + typeof COMPUTE_MONOMIAL_SCHEMA + >, + ); + + // Assert + // ignore linter error caused by vitest-mock-extended + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockCalculatorService.monomial).toHaveBeenCalledExactlyOnceWith( + mockReq.body.a, + mockReq.body.op, + ); + expect(mockReply.code).toHaveBeenCalledExactlyOnceWith(200); + expect(mockReply.send).toHaveBeenCalledExactlyOnceWith({ + result, + reqId: testRequestId, + }); + }); + }); +}); diff --git a/apps/session-manager/tests/unit/server/features/calculator/calculator.service.test.ts b/apps/session-manager/tests/unit/server/features/calculator/calculator.service.test.ts new file mode 100644 index 0000000..93d5be7 --- /dev/null +++ b/apps/session-manager/tests/unit/server/features/calculator/calculator.service.test.ts @@ -0,0 +1,82 @@ +import { beforeEach, describe, expect } from 'vitest'; +import { type MockProxy, mock } from 'vitest-mock-extended'; + +import type { BaseLogger } from '@scribear/base-fastify-server'; + +import CalculatorService from '../../../../../src/server/features/calculator/calculator.service.js'; + +describe('CalculatorService', () => { + let calculatorService: CalculatorService; + let mockLogger: MockProxy; + + beforeEach(() => { + mockLogger = mock(); + calculatorService = new CalculatorService(mockLogger); + }); + + describe('binomial', (it) => { + /** + * Test that calculator adds two numbers correctly + */ + it('correctly adds two numbers', () => { + // Arrange + const a = 10; + const b = 5; + const op = '+'; + + // Act + const result = calculatorService.binomial(a, b, op); + + // Assert + expect(result).toBe(15); + }); + + /** + * Test that calculator subtracts two numbers correctly + */ + it('correctly subtracts two numbers', () => { + // Arrange + const a = 10; + const b = 5; + const op = '-'; + + // Act + const result = calculatorService.binomial(a, b, op); + + // Assert + expect(result).toBe(5); + }); + }); + + describe('monomial', (it) => { + /** + * Test that calculator squares a number correctly + */ + it('correctly squares a number', () => { + // Arrange + const a = 5; + const op = 'square'; + + // Act + const result = calculatorService.monomial(a, op); + + // Assert + expect(result).toBe(25); + }); + + /** + * Test that calculator cubes a number correctly + */ + it('correctly cubes a number', () => { + // Arrange + const a = 3; + const op = 'cube'; + + // Act + const result = calculatorService.monomial(a, op); + + // Assert + expect(result).toBe(27); + }); + }); +}); diff --git a/apps/session-manager/tests/unit/server/features/healthcheck/healthcheck.controller.test.ts b/apps/session-manager/tests/unit/server/features/healthcheck/healthcheck.controller.test.ts new file mode 100644 index 0000000..cf8367d --- /dev/null +++ b/apps/session-manager/tests/unit/server/features/healthcheck/healthcheck.controller.test.ts @@ -0,0 +1,45 @@ +import { type Mock, beforeEach, describe, expect, vi } from 'vitest'; + +import type { + BaseFastifyReply, + BaseFastifyRequest, +} from '@scribear/base-fastify-server'; +import type { HEALTHCHECK_SCHEMA } from '@scribear/session-manager-schema'; + +import HealthcheckController from '../../../../../src/server/features/healthcheck/healthcheck.controller.js'; + +describe('Healthcheck controller', (it) => { + const testRequestId = 'TEST_REQUEST_ID'; + let mockReply: { + send: Mock; + code: Mock; + }; + + let healthcheckController: HealthcheckController; + + beforeEach(() => { + mockReply = { + send: vi.fn(), + code: vi.fn().mockReturnThis(), + }; + + healthcheckController = new HealthcheckController(); + }); + + it('responds with request id', () => { + // Arrange + const mockReq = { id: testRequestId }; + + // Act + healthcheckController.healthcheck( + mockReq as unknown as BaseFastifyRequest, + mockReply as unknown as BaseFastifyReply, + ); + + // Assert + expect(mockReply.code).toHaveBeenCalledExactlyOnceWith(200); + expect(mockReply.send).toHaveBeenCalledExactlyOnceWith({ + reqId: testRequestId, + }); + }); +}); diff --git a/apps/session-manager/tsconfig.json b/apps/session-manager/tsconfig.json new file mode 100644 index 0000000..3475fc4 --- /dev/null +++ b/apps/session-manager/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + }, + "include": [ + "src", + "tests", + ], + // Link to dependencies + "references": [ + { + "path": "../../libs/base-fastify-server" + }, + { + "path": "../../libs/session-manager-schema" + } + ] +} diff --git a/apps/session-manager/vitest.config.ts b/apps/session-manager/vitest.config.ts new file mode 100644 index 0000000..cb09d16 --- /dev/null +++ b/apps/session-manager/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineProject, mergeConfig } from 'vitest/config'; + +import sharedConfig from '../../vitest.shared.js'; + +export default mergeConfig( + sharedConfig, + defineProject({ + test: { + environment: 'node', + }, + }), +); diff --git a/deployment/.gitignore b/deployment/.gitignore deleted file mode 100644 index 34ad749..0000000 --- a/deployment/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -logs -.env -device_config.json \ No newline at end of file diff --git a/deployment/Caddyfile b/deployment/Caddyfile deleted file mode 100644 index af034b8..0000000 --- a/deployment/Caddyfile +++ /dev/null @@ -1,11 +0,0 @@ -{$DOMAIN} { - tls /etc/caddy/pub.cert /etc/caddy/priv.key - - handle /api/* { - reverse_proxy http://node-server:80 - } - - handle { - reverse_proxy http://frontend:80 - } -} diff --git a/deployment/aio-autostart.sh b/deployment/aio-autostart.sh deleted file mode 100755 index 472173d..0000000 --- a/deployment/aio-autostart.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -set -e - -NODE_PORT=8080 -SOURCE_TOKEN=CHANGEME -SCRIBEAR_URL=http://localhost:3000 - -SCRIPT_DIR=$(dirname $0) -cd $SCRIPT_DIR -BASE_DIR=$(pwd) - -# Ensure whisper service and node server are stopped when script exits -stop_children() { - kill $PYTHON_PID - kill $NODE_PID - - wait $PYTHON_PID - wait $NODE_PID -} -trap stop_children EXIT - - -# Create location for logs -mkdir -p $BASE_DIR/logs - - -echo "Starting Whisper Service" -cd $BASE_DIR/whisper-service; -source .venv/bin/activate; -python index.py 2>> $BASE_DIR/logs/whisper-service.log & -PYTHON_PID=$! - - -echo "Starting Node Server" -cd $BASE_DIR/node-server; -node ./build/src/index.js >> $BASE_DIR/logs/node-server.log & -NODE_PID=$! -sleep 15 - - -echo "Launching Chrome" -google-chrome "https://scribear.illinois.edu/v/latest/?mode=kiosk&kioskServerAddress=localhost:${NODE_PORT}&sourceToken=${SOURCE_TOKEN}&scribearURL=${SCRIBEAR_URL}" --start-fullscreen \ No newline at end of file diff --git a/deployment/compose.yaml b/deployment/compose.yaml deleted file mode 100644 index 8d1ac93..0000000 --- a/deployment/compose.yaml +++ /dev/null @@ -1,20 +0,0 @@ -services: - caddy: - image: caddy:2.10-alpine - restart: unless-stopped - ports: - - "80:80" - - "443:443" - - "443:443/udp" - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile - - /etc/ssl/certs/engrit/dcl-1265-05.cs.illinois.edu.cer:/etc/caddy/pub.cert - - /etc/ssl/private/engrit/dcl-1265-05.cs.illinois.edu.key:/etc/caddy/priv.key - - caddy_data:/data - - caddy_config:/config - environment: - - DOMAIN=${DOMAIN} - -volumes: - caddy_data: - caddy_config: diff --git a/deployment/compose_cpu.yaml b/deployment/compose_cpu.yaml deleted file mode 100644 index 07e1e80..0000000 --- a/deployment/compose_cpu.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: 'scribear-server' -services: - whisper-service: - image: scribear/whisper-service-cpu:main - environment: - - LOG_LEVEL=${LOG_LEVEL} - - API_KEY=${API_KEY} - expose: - - 80 - volumes: - - ./device_config.json:/app/device_config.json - restart: unless-stopped - - node-server: - image: scribear/node-server:main - environment: - - NODE_ENV=${NODE_ENV} - - LOG_LEVEL=${LOG_LEVEL} - - USE_HTTPS=${USE_HTTPS} - - CORS_ORIGIN=${CORS_ORIGIN} - - SERVER_ADDRESS=${SERVER_ADDRESS} - - WHISPER_SERVICE_ENDPOINT=ws://whisper-service:80/sourcesink - - API_KEY=${API_KEY} - - REQUIRE_AUTH=${REQUIRE_AUTH} - - SOURCE_TOKEN=${SOURCE_TOKEN} - - ACCESS_TOKEN_BYTES=${ACCESS_TOKEN_BYTES} - - ACCESS_TOKEN_REFRESH_INTERVAL_SEC=${ACCESS_TOKEN_REFRESH_INTERVAL_SEC} - - ACCESS_TOKEN_VALID_PERIOD_SEC=${ACCESS_TOKEN_VALID_PERIOD_SEC} - - SESSION_TOKEN_BYTES=${SESSION_TOKEN_BYTES} - - SESSION_LENGTH_SEC=${SESSION_LENGTH_SEC} - ports: - - "80" - restart: unless-stopped - - frontend: - depends_on: - whisper-service: - condition: service_healthy - node-server: - condition: service_healthy - ports: - - "80" - image: scribear/frontend:master - restart: unless-stopped \ No newline at end of file diff --git a/deployment/compose_cuda.yaml b/deployment/compose_cuda.yaml deleted file mode 100644 index 4db6e4b..0000000 --- a/deployment/compose_cuda.yaml +++ /dev/null @@ -1,54 +0,0 @@ -name: 'scribear-server' -services: - whisper-service: - image: scribear/whisper-service-cuda:main - environment: - - LOG_LEVEL=${LOG_LEVEL} - - API_KEY=${API_KEY} - expose: - - 80 - volumes: - - ./device_config.json:/app/device_config.json - restart: unless-stopped - deploy: - resources: - reservations: - devices: - - driver: nvidia - count: 'all' - capabilities: [gpu] - - node-server: - image: scribear/node-server:main - build: - context: ./node-server - dockerfile: Dockerfile - environment: - - NODE_ENV=${NODE_ENV} - - LOG_LEVEL=${LOG_LEVEL} - - USE_HTTPS=${USE_HTTPS} - - CORS_ORIGIN=${CORS_ORIGIN} - - SERVER_ADDRESS=${SERVER_ADDRESS} - - WHISPER_SERVICE_ENDPOINT=ws://whisper-service:80/sourcesink - - API_KEY=${API_KEY} - - REQUIRE_AUTH=${REQUIRE_AUTH} - - SOURCE_TOKEN=${SOURCE_TOKEN} - - ACCESS_TOKEN_BYTES=${ACCESS_TOKEN_BYTES} - - ACCESS_TOKEN_REFRESH_INTERVAL_SEC=${ACCESS_TOKEN_REFRESH_INTERVAL_SEC} - - ACCESS_TOKEN_VALID_PERIOD_SEC=${ACCESS_TOKEN_VALID_PERIOD_SEC} - - SESSION_TOKEN_BYTES=${SESSION_TOKEN_BYTES} - - SESSION_LENGTH_SEC=${SESSION_LENGTH_SEC} - ports: - - "80" - restart: unless-stopped - - frontend: - depends_on: - whisper-service: - condition: service_healthy - node-server: - condition: service_healthy - ports: - - "80" - image: scribear/frontend:master - restart: unless-stopped \ No newline at end of file diff --git a/deployment/device_config.template.json b/deployment/device_config.template.json deleted file mode 100644 index 1031c95..0000000 --- a/deployment/device_config.template.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "mock_transcription_duration": { - "display_name": "Sanity Test", - "description": "Returns how many seconds of audio was received by whisper service.", - "implementation_id": "mock_transcription_duration", - "implementation_configuration": {}, - "available_features": {} - }, - "faster-whisper:cpu-tiny-en": { - "display_name": "Tiny Faster Whisper", - "description": "Faster Whisper implementation of Open AI Whisper tiny.en model.", - "implementation_id": "faster_whisper", - "implementation_configuration": { - "model": "tiny.en", - "device": "cpu", - "local_agree_dim": 2, - "min_new_samples": 48000, - "max_segment_samples": 480000, - "silence_threshold": 0.01 - }, - "available_features": {} - } -} diff --git a/deployment/docker-autostart.sh b/deployment/docker-autostart.sh deleted file mode 100755 index 24d4eda..0000000 --- a/deployment/docker-autostart.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -set -e - -source .env -if [ $WHISPER_SERVICE_CUDA = 'true' ]; then - echo Pulling images - sudo docker compose -f compose.yaml -f ./compose_cuda.yaml pull - - echo Stopping any currently running services - sudo docker compose -f compose.yaml -f ./compose_cuda.yaml down - - echo Starting services with CUDA enabled for whisper-service - sudo docker compose -f compose.yaml -f ./compose_cuda.yaml up -d -else - echo Pulling images - sudo docker compose -f compose.yaml -f ./compose_cpu.yaml pull - echo Stopping any currently running services - sudo docker compose -f compose.yaml -f ./compose_cpu.yaml down - - echo Starting services without CUDA - - sudo docker compose -f compose.yaml -f ./compose_cpu.yaml up -d -fi - -until [ "$(curl --max-time 1 -s -w '%{http_code}' -o /dev/null "${DOMAIN}")" -eq 200 ] -do - echo "Can't reach frontend at ${DOMAIN}. Waiting for frontend to be ready..." - sleep 1 -done - -echo "Launching Chrome" -echo "${DOMAIN}/?mode=kiosk&kioskServerAddress=${SERVER_ADDRESS}&sourceToken=${SOURCE_TOKEN}&scribearURL=${SCRIBEAR_URL}" --start-fullscreen -google-chrome "${DOMAIN}/?mode=kiosk&kioskServerAddress=${SERVER_ADDRESS}&sourceToken=${SOURCE_TOKEN}&scribearURL=${SCRIBEAR_URL}" --start-fullscreen - -stop_services() { - if [ $WHISPER_SERVICE_CUDA = 'true' ]; then - echo Stopping currently running services - sudo docker compose -f ./compose_cuda.yaml down - else - echo Stopping any currently running services - sudo docker compose -f ./compose_cpu.yaml down - fi -} -trap stop_services EXIT \ No newline at end of file diff --git a/deployment/template.env b/deployment/template.env deleted file mode 100644 index cdf9adf..0000000 --- a/deployment/template.env +++ /dev/null @@ -1,36 +0,0 @@ -NODE_ENV="production" -LOG_LEVEL="info" -DOMAIN="dcl-1265-05.cs.illinois.edu" - -#### Host and port API webserver should listen on -CORS_ORIGIN='*' -SERVER_ADDRESS="192.168.10.160:8080" - -WHISPER_RECONNECT_INTERVAL_SEC=1 - -#### Authentication settings -# Enable or disable authentication -REQUIRE_AUTH=false -# Key used by frontend to connect as audio source -SOURCE_TOKEN="CHANGEME" -# How many bytes of random data should be used to generate access token -ACCESS_TOKEN_BYTES=8 -# How often access token should be refreshed in seconds -ACCESS_TOKEN_REFRESH_INTERVAL_SEC=150 -# How long a single access token is valid for after generation in seconds -ACCESS_TOKEN_VALID_PERIOD_SEC=300 -# How many bytes of random data should be used to generate session token -SESSION_TOKEN_BYTES=32 -# How long a single session token is valid for after generation in seconds -SESSION_LENGTH_SEC=5400 - -# Modified Node Server config -NODE_PORT=8080 -MODEL_KEY="faster-whisper:cpu-tiny-en" -API_KEY="CHANGEME" -WHISPER_SERVICE_CUDA=true - - -# Frontend config -FRONTEND_PORT=3000 -SCRIBEAR_URL="http://192.168.10.160:3000" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6ff17a4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,171 @@ +import eslintReact from '@eslint-react/eslint-plugin'; +import js from '@eslint/js'; +import eslintConfigPrettierFlat from 'eslint-config-prettier/flat'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +/** + * @see https://typescript-eslint.io/getting-started/#step-2-configuration + * @see https://typescript-eslint.io/users/configs + * @see https://eslint.org/docs/latest/use/configure/ + * + * This config object configures ESLint + * ESLint is a code linter that analyzes code before it is run + * This helps + * - Catch potential errors + * - Mantain code quality + */ +export default defineConfig([ + // Files to ignore when linting + // globalIgnores(['**/dist/', '**/coverage/', '**/vitest.config.ts']), + { + // Only lint Typescript files + files: ['**/*.{ts,mts,cts,tsx}'], + extends: [ + /** + * @see https://eslint.org/docs/latest/use/configure/configuration-files#using-predefined-configurations + * Uses recommended ESLint rules for Javscript provided by ESLint + */ + js.configs.recommended, + + /** + * @see https://typescript-eslint.io/users/configs#strict-type-checked + * Uses strict ESLint rules to prevent bugs provided by typescript-eslint + */ + tseslint.configs.strictTypeChecked, + + /** + * @see https://typescript-eslint.io/users/configs#stylistic-type-checked + * Uses ESLint rules for Typescript best practices provided by typescript-eslint + */ + tseslint.configs.stylisticTypeChecked, + + /** + * @see https://react.dev/reference/rules/rules-of-hooks + * @see https://react.dev/learn/react-compiler/installation#eslint-integration + * Allows ESLint to enforce the "Rules of Hooks" + */ + reactHooks.configs.flat['recommended-latest'], + + /** + * @see https://github.com/ArnaudBarre/eslint-plugin-react-refresh + * Allows ESLint to ensure your components can be safely updated with Fast Refresh + */ + reactRefresh.configs.vite, + + /** + * @see https://eslint-react.xyz/docs/presets#typescript-specialized + * Allows ESLint to enforce rules that are recommended general purpose React + React DOM projects + */ + eslintReact.configs['recommended-type-checked'], + + /** + * @see https://github.com/prettier/eslint-config-prettier + * Disables ESLint rules that conflict with Prettier + */ + eslintConfigPrettierFlat, + ], + languageOptions: { + /** + * @see https://typescript-eslint.io/getting-started/typed-linting + * Typescript parsing is required for type checked ESLint rules + */ + parserOptions: { + projectService: true, + }, + // Match tsconfig.json version target + ecmaVersion: 2024, + // Configures global variables ESLint should be aware of + globals: { + // Include global variables provided by browsers + ...globals.browser, + // Include global variables provided by node + ...globals.node, + }, + }, + rules: { + /** + * @see https://typescript-eslint.io/rules/naming-convention/ + * A collection of rules for consistent identifier naming + */ + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: [ + 'accessor', + 'parameter', + 'classProperty', + 'classMethod', + 'variable', + ], + // Prefer camelCase but allow UPPER_CASE for global variables + format: ['camelCase', 'UPPER_CASE'], + }, + { + selector: ['function'], + // React components should be PascalCase, otherwise camelCase + format: ['PascalCase', 'camelCase'], + }, + { + selector: ['class', 'interface'], + format: ['PascalCase'], + }, + { + selector: ['enum'], + format: ['PascalCase'], + }, + { + selector: ['enumMember'], + format: ['UPPER_CASE'], + }, + { + // Ensure private class attributes are identified by leading underscore + selector: ['accessor', 'classProperty', 'classMethod'], + modifiers: ['private'], + format: null, + leadingUnderscore: 'require', + trailingUnderscore: 'forbid', + }, + { + // Ensure protected class attributes are identified by leading underscore + selector: ['accessor', 'classProperty', 'classMethod'], + modifiers: ['protected'], + format: null, + leadingUnderscore: 'require', + trailingUnderscore: 'forbid', + }, + { + // Ensure protected class attributes do not have leading/trailing underscore + selector: ['accessor', 'classProperty', 'classMethod'], + modifiers: ['public'], + format: null, + leadingUnderscore: 'forbid', + trailingUnderscore: 'forbid', + }, + { + // Ensure all other identifiers do not have leading/trailing underscore + selector: [ + 'class', + 'enum', + 'enumMember', + 'function', + 'interface', + 'objectLiteralMethod', + 'objectLiteralProperty', + 'typeAlias', + 'typeMethod', + 'typeParameter', + 'typeProperty', + 'variable', + ], + format: null, + leadingUnderscore: 'forbid', + trailingUnderscore: 'forbid', + }, + ], + }, + }, +]); diff --git a/libs/base-fastify-server/package.json b/libs/base-fastify-server/package.json new file mode 100644 index 0000000..e7c2174 --- /dev/null +++ b/libs/base-fastify-server/package.json @@ -0,0 +1,35 @@ +{ + "name": "@scribear/base-fastify-server", + "version": "0.0.0", + "description": "", + "author": "scribear", + "main": "dist/src/index.js", + "type": "module", + "scripts": { + "build": "tsc --build", + "dev": "tsc --build --watch", + "format": "prettier --check ./src ./tests", + "format:fix": "prettier --write ./src ./tests", + "lint": "eslint ./src ./tests", + "lint:fix": "eslint ./src ./tests --fix", + "test:dev": "vitest --ui --coverage", + "test": "vitest run" + }, + "dependencies": { + "@fastify/awilix": "8.0.0", + "@fastify/cookie": "11.0.2", + "@fastify/helmet": "13.0.2", + "@fastify/sensible": "6.0.3", + "@fastify/swagger": "9.5.2", + "@fastify/swagger-ui": "5.2.3", + "@fastify/type-provider-typebox": "6.1.0", + "@fastify/websocket": "11.2.0", + "awilix": "12.0.5", + "fastify": "5.6.1", + "fastify-plugin": "5.1.0", + "pino": "10.1.0", + "typebox": "1.0.41", + "uuid": "13.0.0", + "ws": "8.18.3" + } +} diff --git a/libs/base-fastify-server/src/index.ts b/libs/base-fastify-server/src/index.ts new file mode 100644 index 0000000..151d7d9 --- /dev/null +++ b/libs/base-fastify-server/src/index.ts @@ -0,0 +1,19 @@ +import createBaseServer from './server/create_base_server.js'; +import type { BaseLogger } from './server/create_logger.js'; +import { LogLevel } from './server/create_logger.js'; +import { BaseHttpError, HttpError } from './server/errors/http_errors.js'; +import type { BaseDependencies } from './server/types/base_dependencies.js'; +import type { + BaseFastifyInstance, + BaseFastifyReply, + BaseFastifyRequest, +} from './server/types/base_fastify_types.js'; + +export { createBaseServer, LogLevel, BaseHttpError, HttpError }; +export type { + BaseLogger, + BaseDependencies, + BaseFastifyInstance, + BaseFastifyReply, + BaseFastifyRequest, +}; diff --git a/libs/base-fastify-server/src/server/create_base_server.ts b/libs/base-fastify-server/src/server/create_base_server.ts new file mode 100644 index 0000000..bfab663 --- /dev/null +++ b/libs/base-fastify-server/src/server/create_base_server.ts @@ -0,0 +1,79 @@ +import { fastifyAwilixPlugin } from '@fastify/awilix'; +import fastifyHelmet from '@fastify/helmet'; +import { fastifySensible } from '@fastify/sensible'; +import { + type AwilixContainer, + InjectionMode, + asValue, + createContainer, +} from 'awilix'; +import Fastify, { type FastifyServerOptions } from 'fastify'; +import { v4 as uuidv4 } from 'uuid'; + +import type { BaseLogger, LogLevel } from './create_logger.js'; +import { createLogger } from './create_logger.js'; +import scopeLogger from './hooks/on_request/scope_logger.js'; +import errorHandler from './plugins/error_handler.js'; +import jsonParser from './plugins/json_parser.js'; +import notFoundHandler from './plugins/not_found_handler.js'; +import schemaValidator from './plugins/schema_validator.js'; +import type { BaseDependencies } from './types/base_dependencies.js'; +import type { BaseFastifyInstance } from './types/base_fastify_types.js'; + +/** + * Creates fastify server, logger, dependency container and loads default plugins and hooks + * @param logLevel Minimum log severity level for created logger + * @param fastifyConfig Additional options for fastify server + * @returns + */ +function createBaseServer( + logLevel: LogLevel, + fastifyConfig?: FastifyServerOptions, +): { + logger: BaseLogger; + dependencyContainer: AwilixContainer; + fastify: BaseFastifyInstance; +} { + const logger = createLogger(logLevel); + + const dependencyContainer: AwilixContainer = + createContainer({ + injectionMode: InjectionMode.CLASSIC, + strict: true, + }); + dependencyContainer.register({ logger: asValue(logger) }); + + const fastify = Fastify({ + loggerInstance: logger, + ...fastifyConfig, + }); + + fastify.register(fastifyAwilixPlugin, { + container: dependencyContainer, + disposeOnClose: true, + disposeOnResponse: true, + strictBooleanEnforced: true, + }); + + // Use UUIDv4 for request ids + fastify.setGenReqId(() => uuidv4()); + + // Register plugins + fastify.register(fastifySensible); + fastify.register(fastifyHelmet); + fastify.register(errorHandler); + fastify.register(jsonParser); + fastify.register(notFoundHandler); + fastify.register(schemaValidator); + + // Register hooks + fastify.register(scopeLogger); + + return { + logger, + dependencyContainer, + fastify: fastify as BaseFastifyInstance, + }; +} + +export default createBaseServer; diff --git a/libs/base-fastify-server/src/server/create_logger.ts b/libs/base-fastify-server/src/server/create_logger.ts new file mode 100644 index 0000000..ce5655a --- /dev/null +++ b/libs/base-fastify-server/src/server/create_logger.ts @@ -0,0 +1,31 @@ +import { pino, stdSerializers } from 'pino'; +import type { Logger } from 'pino'; + +// Alias type to decouple log provider from application +type BaseLogger = Logger; + +enum LogLevel { + SILENT = 'silent', + TRACE = 'trace', + DEBUG = 'debug', + INFO = 'info', + WARN = 'warn', + ERROR = 'error', + FATAL = 'fatal', +} + +/** + * Creates a logger instance using app configuration + * @param logLevel Minimum severity level that should be logged + * @returns created logger + */ +function createLogger(logLevel: LogLevel): BaseLogger { + const logger = pino({ + level: logLevel, + serializers: { err: stdSerializers.errWithCause }, + }); + return logger; +} + +export type { BaseLogger }; +export { createLogger, LogLevel }; diff --git a/libs/base-fastify-server/src/server/errors/http_errors.ts b/libs/base-fastify-server/src/server/errors/http_errors.ts new file mode 100644 index 0000000..bbb174f --- /dev/null +++ b/libs/base-fastify-server/src/server/errors/http_errors.ts @@ -0,0 +1,62 @@ +type HttpErrorCodes = 400 | 401 | 403 | 404 | 429 | 500; + +/** + * Base class of errors thrown for fastify error handler use + * Message should be a user facing error message + */ +class BaseHttpError extends Error { + statusCode: HttpErrorCodes = 500; + code = 'HTTP_ERROR'; + override message = + 'Server encountered an unexpected error. Please try again later.'; +} + +/** + * BadRequest error that contains list of error messages and validation error keys + */ +class HttpBadRequest extends BaseHttpError { + override statusCode = 400 as const; + override message = 'Bad Request'; + constructor( + public requestErrors: { + message: string; + key: string; + }[], + ) { + super(); + if (Error.stackTraceLimit !== 0) { + Error.captureStackTrace(this, HttpBadRequest); + } + } +} + +/** + * Produces custom http error class derived from HttpError with statusCode set + * @param statusCode http status code of error to create + * @returns custom http error class + */ +function createHttpError(statusCode: HttpErrorCodes) { + return class CustomHttpError extends BaseHttpError { + constructor(message?: string) { + super(); + if (message !== undefined) this.message = message; + this.statusCode = statusCode; + if (Error.stackTraceLimit !== 0) { + Error.captureStackTrace(this, CustomHttpError); + } + } + }; +} + +/* eslint-disable @typescript-eslint/naming-convention */ +const HttpError = { + BadRequest: HttpBadRequest, + Unauthorized: createHttpError(401), + Forbidden: createHttpError(403), + NotFound: createHttpError(404), + TooManyRequests: createHttpError(429), + ServerError: createHttpError(500), +}; +/* eslint-enable @typescript-eslint/naming-convention */ + +export { BaseHttpError, HttpError }; diff --git a/libs/base-fastify-server/src/server/hooks/on_request/scope_logger.ts b/libs/base-fastify-server/src/server/hooks/on_request/scope_logger.ts new file mode 100644 index 0000000..1fe9050 --- /dev/null +++ b/libs/base-fastify-server/src/server/hooks/on_request/scope_logger.ts @@ -0,0 +1,16 @@ +import { asValue } from 'awilix'; +import fastifyPlugin from 'fastify-plugin'; + +import type { BaseLogger } from '../../create_logger.js'; +import type { BaseFastifyInstance } from '../../types/base_fastify_types.js'; + +/** + * Fastify onRequest hook that registers logger with request scoped dependency container + * Allows logger to include reqId context + */ +export default fastifyPlugin((fastify: BaseFastifyInstance) => { + fastify.addHook('onRequest', (req, reply, done) => { + req.diScope.register({ logger: asValue(req.log as BaseLogger) }); + done(); + }); +}); diff --git a/libs/base-fastify-server/src/server/plugins/error_handler.ts b/libs/base-fastify-server/src/server/plugins/error_handler.ts new file mode 100644 index 0000000..b9c0e4c --- /dev/null +++ b/libs/base-fastify-server/src/server/plugins/error_handler.ts @@ -0,0 +1,55 @@ +import { FastifyError } from '@fastify/error'; +import fastifyPlugin from 'fastify-plugin'; + +import { SHARED_ERROR_REPLY_SCHEMA } from '@scribear/base-schema'; + +import { BaseHttpError, HttpError } from '../errors/http_errors.js'; +import type { + BaseFastifyInstance, + BaseFastifyReply, + BaseFastifyRequest, +} from '../types/base_fastify_types.js'; + +/** + * Custom fastify error handler + * Ensure BaseHttpErrors return correctly formatted responses + * All other errors are caught and return InternalServerError response + * Fastify errors are rethrown to be handled by fastify's default error handler + */ +export default fastifyPlugin((fastify: BaseFastifyInstance) => { + fastify.setErrorHandler( + ( + err: unknown, + req: BaseFastifyRequest<{ response: typeof SHARED_ERROR_REPLY_SCHEMA }>, + reply: BaseFastifyReply<{ response: typeof SHARED_ERROR_REPLY_SCHEMA }>, + ) => { + // Let default error handler manage FastifyErrors + if (err instanceof FastifyError) throw err; + + if (!(err instanceof BaseHttpError)) { + // If not BaseHttpError, return Internal Server Error + req.log.info({ + msg: 'Request encountered internal server error', + err, + }); + + return reply.code(500).send({ + message: + 'Sever encountered an unexpected error. Please try again later.', + reqId: req.id, + }); + } + + // If HttpBadRequest, include requestErrors in response + if (err instanceof HttpError.BadRequest) { + return reply + .code(err.statusCode) + .send({ requestErrors: err.requestErrors, reqId: req.id }); + } + + return reply + .code(err.statusCode) + .send({ message: err.message, reqId: req.id }); + }, + ); +}); diff --git a/libs/base-fastify-server/src/server/plugins/json_parser.ts b/libs/base-fastify-server/src/server/plugins/json_parser.ts new file mode 100644 index 0000000..53cae01 --- /dev/null +++ b/libs/base-fastify-server/src/server/plugins/json_parser.ts @@ -0,0 +1,28 @@ +import fastifyPlugin from 'fastify-plugin'; + +import { HttpError } from '../errors/http_errors.js'; +import type { BaseFastifyInstance } from '../types/base_fastify_types.js'; + +/** + * Custom fastify not found handler to throw custom 400 error + */ +export default fastifyPlugin((fastify: BaseFastifyInstance) => { + fastify.addContentTypeParser( + 'application/json', + { parseAs: 'buffer' }, + (req, body, done) => { + try { + const parsedBody: unknown = JSON.parse(body.toString()); + done(null, parsedBody); + } catch (parseError) { + req.log.info({ msg: 'Failed to parse JSON body', err: parseError }); + + done( + new HttpError.BadRequest([ + { message: 'Invalid JSON found in request body.', key: '/body' }, + ]), + ); + } + }, + ); +}); diff --git a/libs/base-fastify-server/src/server/plugins/not_found_handler.ts b/libs/base-fastify-server/src/server/plugins/not_found_handler.ts new file mode 100644 index 0000000..66325b1 --- /dev/null +++ b/libs/base-fastify-server/src/server/plugins/not_found_handler.ts @@ -0,0 +1,14 @@ +import type { FastifyRequest } from 'fastify'; +import fastifyPlugin from 'fastify-plugin'; + +import { HttpError } from '../errors/http_errors.js'; +import type { BaseFastifyInstance } from '../types/base_fastify_types.js'; + +/** + * Custom fastify not found handler to throw custom 404 error + */ +export default fastifyPlugin((fastify: BaseFastifyInstance) => { + fastify.setNotFoundHandler((req: FastifyRequest) => { + throw new HttpError.NotFound(`Route ${req.method}: ${req.url} not found`); + }); +}); diff --git a/libs/base-fastify-server/src/server/plugins/schema_validator.ts b/libs/base-fastify-server/src/server/plugins/schema_validator.ts new file mode 100644 index 0000000..5a52072 --- /dev/null +++ b/libs/base-fastify-server/src/server/plugins/schema_validator.ts @@ -0,0 +1,44 @@ +import { + type TSchema, + type TypeBoxTypeProvider, + TypeBoxValidatorCompiler, +} from '@fastify/type-provider-typebox'; +import fastifyPlugin from 'fastify-plugin'; +import type { FastifyRouteSchemaDef } from 'fastify/types/schema.js'; + +import { HttpError } from '../errors/http_errors.js'; +import type { BaseFastifyInstance } from '../types/base_fastify_types.js'; + +/** + * Custom fastify schema validator to throw custom 400 error + */ +export default fastifyPlugin((fastify: BaseFastifyInstance) => { + fastify.withTypeProvider(); + + fastify.setValidatorCompiler((schemaDef) => { + const validator = TypeBoxValidatorCompiler( + schemaDef as FastifyRouteSchemaDef, + ); + + return (...args) => { + const result = validator(...args) as { + value: unknown; + error?: { message: string; instancePath: string }[]; + }; + + if (!result.error) { + return { value: result.value }; + } + + const requestErrors = result.error.map(({ message, instancePath }) => { + return { + message, + key: `/${schemaDef.httpPart ?? ''}${instancePath}`, + }; + }); + return { + error: new HttpError.BadRequest(requestErrors), + }; + }; + }); +}); diff --git a/libs/base-fastify-server/src/server/types/base_dependencies.ts b/libs/base-fastify-server/src/server/types/base_dependencies.ts new file mode 100644 index 0000000..36673bc --- /dev/null +++ b/libs/base-fastify-server/src/server/types/base_dependencies.ts @@ -0,0 +1,23 @@ +import type { BaseLogger } from '../create_logger.js'; + +/** + * Define the dependencies in dependency container provided by base fastify server + */ +interface BaseDependencies { + logger: BaseLogger; +} + +/** + * Ensure fastify awilix container is typed correctly + * Applications using this base fastify instance should extend BaseDependencies with their own dependencies + * @see https://github.com/fastify/fastify-awilix?tab=readme-ov-file#typescript-usage + */ +declare module '@fastify/awilix' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface Cradle extends BaseDependencies {} + + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface RequestCradle extends BaseDependencies {} +} + +export type { BaseDependencies }; diff --git a/libs/base-fastify-server/src/server/types/base_fastify_types.ts b/libs/base-fastify-server/src/server/types/base_fastify_types.ts new file mode 100644 index 0000000..70ad6e2 --- /dev/null +++ b/libs/base-fastify-server/src/server/types/base_fastify_types.ts @@ -0,0 +1,46 @@ +import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; +import type { + ContextConfigDefault, + FastifyInstance, + FastifyReply, + FastifyRequest, + RawReplyDefaultExpression, + RawRequestDefaultExpression, + RawServerDefault, +} from 'fastify'; +import type { RouteGenericInterface } from 'fastify/types/route.js'; +import type { FastifySchema } from 'fastify/types/schema.js'; + +import type { BaseLogger } from '../create_logger.js'; + +/** + * Custom types for Fastify to provide type checking for requests and replies according to route schema + * @see https://github.com/fastify/fastify-type-provider-typebox?tab=readme-ov-file#type-definition-of-fastifyrequest-fastifyreply--typeprovider + */ +type BaseFastifyInstance = FastifyInstance< + RawServerDefault, + RawRequestDefaultExpression, + RawReplyDefaultExpression, + BaseLogger, + TypeBoxTypeProvider +>; + +type BaseFastifyReply = FastifyReply< + RouteGenericInterface, + RawServerDefault, + RawRequestDefaultExpression, + RawReplyDefaultExpression, + ContextConfigDefault, + TSchema, + TypeBoxTypeProvider +>; + +type BaseFastifyRequest = FastifyRequest< + RouteGenericInterface, + RawServerDefault, + RawRequestDefaultExpression, + TSchema, + TypeBoxTypeProvider +>; + +export type { BaseFastifyInstance, BaseFastifyRequest, BaseFastifyReply }; diff --git a/libs/base-fastify-server/tests/integration/dependency_injection.test.ts b/libs/base-fastify-server/tests/integration/dependency_injection.test.ts new file mode 100644 index 0000000..f93df73 --- /dev/null +++ b/libs/base-fastify-server/tests/integration/dependency_injection.test.ts @@ -0,0 +1,58 @@ +import { asValue } from 'awilix'; +import type { AwilixContainer } from 'awilix'; +import { beforeEach, describe, expect } from 'vitest'; + +import { LogLevel } from '../../src/index.js'; +import createBaseServer from '../../src/server/create_base_server.js'; +import type { BaseDependencies } from '../../src/server/types/base_dependencies.js'; +import type { BaseFastifyInstance } from '../../src/server/types/base_fastify_types.js'; + +interface TestDependencies extends BaseDependencies { + test: string; +} + +describe('Integration Tests - Dependency Injection', (it) => { + let fastify: BaseFastifyInstance; + let container: AwilixContainer; + + beforeEach(() => { + const server = createBaseServer(LogLevel.SILENT); + + fastify = server.fastify; + container = server.dependencyContainer as AwilixContainer; + }); + + /** + * Test that dependency container contains logger + */ + it('makes logger available in dependency container', () => { + // Arrange + // Act + const logger = container.resolve('logger'); + + // Assert + expect(logger).not.toBeUndefined(); + }); + + /** + * Test that dependency container makes registered dependencies available in request container scope + */ + it('makes registered dependencies available in request handlers', async () => { + // Arrange + const testValue = 'TEST_VALUE'; + container.register({ test: asValue(testValue) }); + fastify.get('/di', (req, res) => { + return res.send(req.diScope.resolve('test')); + }); + + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/di', + }); + + // Assert + expect(response.statusCode).toBe(200); + expect(response.payload).toBe(testValue); + }); +}); diff --git a/libs/base-fastify-server/tests/integration/error_handling.test.ts b/libs/base-fastify-server/tests/integration/error_handling.test.ts new file mode 100644 index 0000000..54b0620 --- /dev/null +++ b/libs/base-fastify-server/tests/integration/error_handling.test.ts @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect } from 'vitest'; + +import { LogLevel } from '../../src/index.js'; +import createBaseServer from '../../src/server/create_base_server.js'; +import { HttpError } from '../../src/server/errors/http_errors.js'; +import type { BaseFastifyInstance } from '../../src/server/types/base_fastify_types.js'; + +/** + * Integration tests for server error handling + */ +describe('Integration Tests - Error Handling', (it) => { + let fastify: BaseFastifyInstance; + + beforeEach(() => { + const server = createBaseServer(LogLevel.SILENT); + + fastify = server.fastify; + }); + + /** + * Test that server returns correct response containg request error when BadRequest error is thrown in handler + */ + it('returns 400 response when BadRequest error is thrown in handler', async () => { + // Arrange + const requestErrors = [ + { message: 'something wrong', key: '/body' }, + { message: 'something else', key: '/body/a' }, + ]; + fastify.get('/error', () => { + throw new HttpError.BadRequest(requestErrors); + }); + + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/error', + }); + + // Assert + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ requestErrors }); + }); + + /** + * Test that server returns correct response when HttpErrors are thrown in handler + */ + it.for([ + { httpError: HttpError.Unauthorized, code: 401, name: 'Unauthorized' }, + { httpError: HttpError.Forbidden, code: 403, name: 'Forbidden' }, + { httpError: HttpError.NotFound, code: 404, name: 'NotFound' }, + { + httpError: HttpError.TooManyRequests, + code: 429, + name: 'TooManyRequests', + }, + { httpError: HttpError.ServerError, code: 500, name: 'ServerError' }, + ])( + 'returns $code response for $name error is thrown in handler', + async ({ httpError, code }) => { + // Arrange + const errorMessage = 'Test unauthorized'; + fastify.get('/error', () => { + throw new httpError(errorMessage); + }); + + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/error', + }); + + // Assert + expect(response.statusCode).toBe(code); + expect(response.json()).toMatchObject({ message: errorMessage }); + }, + ); + + /** + * Test that server returns correct response when an invalid route is requested + */ + it('returns 404 response when path is not found', async () => { + // Arrange + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/error/notfound', + }); + + // Assert + expect(response.statusCode).toBe(404); + expect(response.json()).toMatchObject({ + message: 'Route GET: /error/notfound not found', + }); + }); + + /** + * Test that server returns correct response when a server error is thrown in handler + */ + it('returns 500 response when general error is thrown in handler', async () => { + // Arrange + fastify.get('/error', () => { + throw new Error('Server Error'); + }); + + // Act + const response = await fastify.inject({ + method: 'GET', + url: '/error', + }); + + // Assert + expect(response.statusCode).toBe(500); + expect(response.json()).toMatchObject({ + message: 'Sever encountered an unexpected error. Please try again later.', + }); + }); +}); diff --git a/libs/base-fastify-server/tests/integration/request_validation.test.ts b/libs/base-fastify-server/tests/integration/request_validation.test.ts new file mode 100644 index 0000000..29d0a80 --- /dev/null +++ b/libs/base-fastify-server/tests/integration/request_validation.test.ts @@ -0,0 +1,89 @@ +import { Type } from 'typebox'; +import { beforeEach, describe, expect } from 'vitest'; + +import { LogLevel } from '../../src/index.js'; +import createBaseServer from '../../src/server/create_base_server.js'; +import type { + BaseFastifyInstance, + BaseFastifyReply, + BaseFastifyRequest, +} from '../../src/server/types/base_fastify_types.js'; + +describe('Integration Tests - Request validation', (it) => { + let fastify: BaseFastifyInstance; + + beforeEach(() => { + const server = createBaseServer(LogLevel.SILENT); + + fastify = server.fastify; + + const schema = { + params: Type.Object({ + id: Type.String({ minLength: 2 }), + }), + querystring: Type.Object({ + string: Type.String(), + }), + body: Type.Object({ + num: Type.Number({ minimum: 18 }), + }), + }; + + fastify.post( + '/schema/:id', + { schema }, + ( + req: BaseFastifyRequest, + reply: BaseFastifyReply, + ) => { + return reply.code(200).send(); + }, + ); + }); + + /** + * Test that valid requests are passed to request handler successfully + */ + it('returns 200 response for valid schema', async () => { + // Arrange + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/schema/some_id?string=string', + headers: { + 'content-type': 'application/json', + }, + payload: JSON.stringify({ num: 18 }), + }); + + // Assert + expect(response.statusCode).toBe(200); + }); + + /** + * Test that invalid requests return 400 error + */ + it('returns 400 response for invalid schema', async () => { + // Arrange + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/schema/some_id', + headers: { + 'content-type': 'application/json', + }, + payload: JSON.stringify({ num: 18 }), + }); + + // Assert + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ + requestErrors: [ + { + key: '/querystring', + message: 'must have required properties string', + }, + ], + }); + }); +}); diff --git a/libs/base-fastify-server/tests/unit/server/hooks/on_request/scope_logger.test.ts b/libs/base-fastify-server/tests/unit/server/hooks/on_request/scope_logger.test.ts new file mode 100644 index 0000000..7a68e00 --- /dev/null +++ b/libs/base-fastify-server/tests/unit/server/hooks/on_request/scope_logger.test.ts @@ -0,0 +1,53 @@ +import type { AwilixContainer } from 'awilix'; +import Fastify from 'fastify'; +import type { FastifyInstance } from 'fastify'; +import { type Mock, beforeEach, describe, expect, vi } from 'vitest'; + +import type { BaseLogger } from '../../../../../src/index.js'; +import scopeLogger from '../../../../../src/server/hooks/on_request/scope_logger.js'; + +// Override awilix asValue function to be a no-op +vi.mock('awilix', () => ({ + asValue: vi.fn((a: unknown) => a), +})); + +describe('Log Request Hook', (it) => { + const testRequestId = 'TEST-REQUST-ID'; + + let fastify: FastifyInstance; + let mockLogger: { child: Mock }; + let mockDiScope: { register: Mock }; + + beforeEach(() => { + mockLogger = { child: vi.fn().mockReturnThis() }; + mockDiScope = { register: vi.fn() }; + + fastify = Fastify({ genReqId: () => testRequestId }); + + fastify.decorateRequest('log', { + getter: () => mockLogger as unknown as BaseLogger, + setter: () => mockLogger, + }); + fastify.decorateRequest('diScope', { + getter: () => mockDiScope as unknown as AwilixContainer, + }); + + fastify.register(scopeLogger); + }); + + /** + * Test that scopeLogger hook registers logger with request scoped dependency container + */ + it('registers logger with dependency container', async () => { + // Arrange / Act + await fastify.inject({ + method: 'GET', + url: '/test/hello/world', + }); + + // Assert + expect(mockDiScope.register).toHaveBeenCalledExactlyOnceWith({ + logger: mockLogger, + }); + }); +}); diff --git a/libs/base-fastify-server/tests/unit/server/plugins/error_handler.test.ts b/libs/base-fastify-server/tests/unit/server/plugins/error_handler.test.ts new file mode 100644 index 0000000..8174111 --- /dev/null +++ b/libs/base-fastify-server/tests/unit/server/plugins/error_handler.test.ts @@ -0,0 +1,119 @@ +import Fastify, { type FastifyInstance, errorCodes } from 'fastify'; +import { beforeEach, describe, expect } from 'vitest'; + +import { HttpError } from '../../../../src/server/errors/http_errors.js'; +import errorHandler from '../../../../src/server/plugins/error_handler.js'; + +describe('Error Handler Plugin', (it) => { + const testRequestId = 'TEST-REQUEST-ID'; + + let fastify: FastifyInstance; + + beforeEach(() => { + fastify = Fastify({ genReqId: () => testRequestId }); + fastify.register(errorHandler); + }); + + /** + * Check that error handler handles HttpErrors by return API response containing message and request id + * Note: BadRequest has a different schema and is tested separately + */ + it.for([ + { httpError: HttpError.Unauthorized, code: 401, name: 'Unauthorized' }, + { httpError: HttpError.Forbidden, code: 403, name: 'Forbidden' }, + { httpError: HttpError.NotFound, code: 404, name: 'NotFound' }, + { + httpError: HttpError.TooManyRequests, + code: 429, + name: 'TooManyRequests', + }, + { httpError: HttpError.ServerError, code: 500, name: 'ServerError' }, + ])( + 'handles $name by returning error message and request id', + async ({ httpError, code }) => { + // Arrange + const errorMessage = 'Resource not found'; + fastify.get('/test', () => { + throw new httpError(errorMessage); + }); + + // Act + const response = await fastify.inject({ method: 'GET', url: '/test' }); + + // Assertions + expect(response.statusCode).toBe(code); + expect(JSON.parse(response.payload)).toStrictEqual({ + message: errorMessage, + reqId: testRequestId, + }); + }, + ); + + /** + * Check that error handler handles BadRequest by returning API response containing request id and list of request errors + */ + it('handles BadRequest by returning list of request errors', async () => { + // Arrange + const testRequestErrors = [ + { key: '/body/email', message: 'Invalid email format' }, + { key: '/body/password', message: 'Password is too short' }, + ]; + + fastify.get('/test', () => { + throw new HttpError.BadRequest(testRequestErrors); + }); + + // Act + const response = await fastify.inject({ method: 'GET', url: '/test' }); + + // Assert + expect(response.statusCode).toBe(400); + expect(JSON.parse(response.payload)).toStrictEqual({ + requestErrors: testRequestErrors, + reqId: testRequestId, + }); + }); + + /** + * Check that error handler lets fastify handle fastify errors + */ + it.for( + Object.entries(errorCodes).map(([key, value]) => { + return { name: key, error: value }; + }), + )('avoids handling fastify error: $name', async ({ error, name }) => { + // Arrange + fastify.get('/test', () => { + throw new error('Fastify threw some error'); + }); + + // Act + const response = await fastify.inject({ method: 'GET', url: '/test' }); + + // Assert + expect(response.json()).toMatchObject({ + // fastify error handler returns error name in code field + code: name, + }); + }); + + /** + * Check that error handler treats all other Errors as a 500 Server Error and returns appropriate response + */ + it('handles non-HttpError as a 500 Server Error', async () => { + // Arrange + fastify.get('/test', () => { + throw new Error('Something went wrong'); + }); + + // Act + const response = await fastify.inject({ method: 'GET', url: '/test' }); + + // Assert + expect(response.statusCode).toBe(500); + expect(JSON.parse(response.payload)).toStrictEqual({ + message: 'Sever encountered an unexpected error. Please try again later.', + reqId: testRequestId, + }); + }); +}); diff --git a/libs/base-fastify-server/tests/unit/server/plugins/json_parser.test.ts b/libs/base-fastify-server/tests/unit/server/plugins/json_parser.test.ts new file mode 100644 index 0000000..0bd4248 --- /dev/null +++ b/libs/base-fastify-server/tests/unit/server/plugins/json_parser.test.ts @@ -0,0 +1,70 @@ +import Fastify, { type FastifyInstance } from 'fastify'; +import { type Mock, beforeEach, describe, expect, vi } from 'vitest'; + +import { HttpError } from '../../../../src/index.js'; +import jsonParser from '../../../../src/server/plugins/json_parser.js'; + +describe('JSON Parser Plugin', (it) => { + let fastify: FastifyInstance; + let mockErrorHandler: Mock; + + beforeEach(() => { + mockErrorHandler = vi.fn().mockReturnValue(''); + + fastify = Fastify(); + + fastify.setErrorHandler(mockErrorHandler); + fastify.register(jsonParser); + + fastify.post('/test', async (req, reply) => { + return reply.send(req.body); + }); + }); + + /** + * Test that jsonParser is able to parse valid json + */ + it('parses a valid JSON body successfully', async () => { + // Arrange + const validPayload = { hello: 'world', count: 123 }; + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/test', + headers: { + 'content-type': 'application/json', + }, + payload: JSON.stringify(validPayload), + }); + + // Assert + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload)).toEqual(validPayload); + }); + + /** + * Test that jsonParser throws BadRequest error if malformed JSON is received + */ + it('throws BadRequest error for a malformed JSON body', async () => { + // Arrange + const invalidPayload = '{"key": "value",}'; + + // Act + await fastify.inject({ + method: 'POST', + url: '/test', + headers: { + 'content-type': 'application/json', + }, + payload: invalidPayload, + }); + + // Assert + expect(mockErrorHandler).toHaveBeenCalledExactlyOnceWith( + expect.any(HttpError.BadRequest), + expect.anything(), + expect.anything(), + ); + }); +}); diff --git a/libs/base-fastify-server/tests/unit/server/plugins/not_found_handler.test.ts b/libs/base-fastify-server/tests/unit/server/plugins/not_found_handler.test.ts new file mode 100644 index 0000000..f6db3c1 --- /dev/null +++ b/libs/base-fastify-server/tests/unit/server/plugins/not_found_handler.test.ts @@ -0,0 +1,37 @@ +import Fastify, { type FastifyInstance } from 'fastify'; +import { type Mock, beforeEach, describe, expect, vi } from 'vitest'; + +import { HttpError } from '../../../../src/index.js'; +import notFoundHandler from '../../../../src/server/plugins/not_found_handler.js'; + +describe('Not Found Handler Plugin', (it) => { + let fastify: FastifyInstance; + let mockErrorHandler: Mock; + + beforeEach(() => { + mockErrorHandler = vi.fn().mockReturnValue(''); + + fastify = Fastify(); + + fastify.setErrorHandler(mockErrorHandler); + fastify.register(notFoundHandler); + }); + + /** + * Test that Not Found handler plugin throws Not Found error when called + */ + it('throws NotFound error', async () => { + // Arrange / Act + await fastify.inject({ + method: 'GET', + url: '/test', + }); + + // Assert + expect(mockErrorHandler).toHaveBeenCalledExactlyOnceWith( + expect.any(HttpError.NotFound), + expect.anything(), + expect.anything(), + ); + }); +}); diff --git a/libs/base-fastify-server/tests/unit/server/plugins/schema_validator.test.ts b/libs/base-fastify-server/tests/unit/server/plugins/schema_validator.test.ts new file mode 100644 index 0000000..8fe0a88 --- /dev/null +++ b/libs/base-fastify-server/tests/unit/server/plugins/schema_validator.test.ts @@ -0,0 +1,91 @@ +import Fastify, { type FastifyInstance } from 'fastify'; +import { Type } from 'typebox'; +import { type Mock, beforeEach, describe, expect, vi } from 'vitest'; + +import { HttpError } from '../../../../src/index.js'; +import jsonParser from '../../../../src/server/plugins/json_parser.js'; +import schemaValidator from '../../../../src/server/plugins/schema_validator.js'; + +describe('Schema Validator Plugin', (it) => { + let fastify: FastifyInstance; + let mockErrorHandler: Mock; + + beforeEach(() => { + mockErrorHandler = vi.fn().mockReturnValue(''); + + fastify = Fastify(); + + fastify.setErrorHandler(mockErrorHandler); + fastify.register(jsonParser); + fastify.register(schemaValidator); + }); + + /** + * Test that schemaValidator is able to parse valid requests + */ + it('successfully parses valid request', async () => { + // Arrange + const validPayload = { string: 'string', num: 123 }; + fastify.post( + '/test', + { + schema: { + body: Type.Object({ string: Type.String(), num: Type.Number() }), + }, + }, + (req, reply) => { + return reply.send(req.body); + }, + ); + + // Act + const response = await fastify.inject({ + method: 'POST', + url: '/test', + headers: { + 'content-type': 'application/json', + }, + payload: JSON.stringify(validPayload), + }); + + // Assert + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.payload)).toEqual(validPayload); + }); + + /** + * Test that schemaValidator throws BadRequest error when parsing invalid requests + */ + it('throws BadRequest error for invalid request', async () => { + // Arrange + const invalidPayload = { string: 'string', num: 'not a num' }; + fastify.post( + '/test', + { + schema: { + body: Type.Object({ string: Type.String(), num: Type.Number() }), + }, + }, + (req, reply) => { + return reply.send(req.body); + }, + ); + + // Act + await fastify.inject({ + method: 'POST', + url: '/test', + headers: { + 'content-type': 'application/json', + }, + payload: JSON.stringify(invalidPayload), + }); + + // Assert + expect(mockErrorHandler).toHaveBeenCalledExactlyOnceWith( + expect.any(HttpError.BadRequest), + expect.anything(), + expect.anything(), + ); + }); +}); diff --git a/libs/base-fastify-server/tsconfig.json b/libs/base-fastify-server/tsconfig.json new file mode 100644 index 0000000..b94b24c --- /dev/null +++ b/libs/base-fastify-server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + }, + "include": [ + "src", + "tests", + ], + // Link to shared packages + "references": [ + { + "path": "../base-schema/tsconfig.json" + } + ] +} diff --git a/libs/base-fastify-server/vitest.config.ts b/libs/base-fastify-server/vitest.config.ts new file mode 100644 index 0000000..cb09d16 --- /dev/null +++ b/libs/base-fastify-server/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineProject, mergeConfig } from 'vitest/config'; + +import sharedConfig from '../../vitest.shared.js'; + +export default mergeConfig( + sharedConfig, + defineProject({ + test: { + environment: 'node', + }, + }), +); diff --git a/libs/base-schema/package.json b/libs/base-schema/package.json new file mode 100644 index 0000000..a6bfbf3 --- /dev/null +++ b/libs/base-schema/package.json @@ -0,0 +1,19 @@ +{ + "name": "@scribear/base-schema", + "version": "0.0.0", + "description": "", + "author": "scribear", + "main": "dist/src/index.js", + "type": "module", + "scripts": { + "build": "tsc --build", + "dev": "tsc --build --watch", + "format": "prettier --check ./src", + "format:fix": "prettier --write ./src", + "lint": "eslint ./src", + "lint:fix": "eslint ./src --fix" + }, + "dependencies": { + "typebox": "1.0.41" + } +} diff --git a/libs/base-schema/src/index.ts b/libs/base-schema/src/index.ts new file mode 100644 index 0000000..bb3fb75 --- /dev/null +++ b/libs/base-schema/src/index.ts @@ -0,0 +1,5 @@ +import { SHARED_ERROR_REPLY_SCHEMA } from './shared/shared_error_reply_schema.js'; +import type { BaseRouteSchema } from './types/base_route_schema.js'; + +export { SHARED_ERROR_REPLY_SCHEMA }; +export type { BaseRouteSchema }; diff --git a/libs/base-schema/src/shared/shared_error_reply_schema.ts b/libs/base-schema/src/shared/shared_error_reply_schema.ts new file mode 100644 index 0000000..45689d7 --- /dev/null +++ b/libs/base-schema/src/shared/shared_error_reply_schema.ts @@ -0,0 +1,71 @@ +import { Type } from 'typebox'; + +/** + * Reply schema of shared API http error responses + */ +const SHARED_ERROR_REPLY_SCHEMA = { + 400: Type.Object( + { + requestErrors: Type.Array( + Type.Object({ + message: Type.String(), + key: Type.String({ default: '/body/some/nested/object/property' }), + }), + ), + reqId: Type.String(), + }, + { + description: + 'Response when request was invalid. Each validation error has a user facing message about the error. Keys start with the part of request that had error ("/body", "/headers", "/params", "/querystring").', + }, + ), + 401: Type.Object( + { + message: Type.String(), + reqId: Type.String(), + }, + { + description: + 'Response when request failed authentication. Message will contain user facing reason why.', + }, + ), + 403: Type.Object( + { + message: Type.String(), + reqId: Type.String(), + }, + { + description: + 'Response when request failed authorization. Message will contain user facing reason why.', + }, + ), + 404: Type.Object( + { + message: Type.String(), + reqId: Type.String(), + }, + { + description: 'Response when request had no matching path on server.', + }, + ), + 429: Type.Object( + { + message: Type.String(), + reqId: Type.String(), + }, + { + description: 'Response when request is rate limited.', + }, + ), + 500: Type.Object( + { + message: Type.String(), + reqId: Type.String(), + }, + { + description: 'Response when server encounters an unexpected error.', + }, + ), +}; + +export { SHARED_ERROR_REPLY_SCHEMA }; diff --git a/libs/base-schema/src/types/base_route_schema.ts b/libs/base-schema/src/types/base_route_schema.ts new file mode 100644 index 0000000..e93af56 --- /dev/null +++ b/libs/base-schema/src/types/base_route_schema.ts @@ -0,0 +1,18 @@ +/** + * Defines a route on server + */ +interface BaseRouteSchema { + method: + | 'GET' + | 'HEAD' + | 'TRACE' + | 'DELETE' + | 'OPTIONS' + | 'PATCH' + | 'PUT' + | 'POST'; + + url: string; +} + +export type { BaseRouteSchema }; diff --git a/libs/base-schema/tsconfig.json b/libs/base-schema/tsconfig.json new file mode 100644 index 0000000..71dbd8f --- /dev/null +++ b/libs/base-schema/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "src", + ], + // Link to dependencies + "references": [] +} diff --git a/libs/session-manager-schema/package.json b/libs/session-manager-schema/package.json new file mode 100644 index 0000000..d91ae83 --- /dev/null +++ b/libs/session-manager-schema/package.json @@ -0,0 +1,19 @@ +{ + "name": "@scribear/session-manager-schema", + "version": "0.0.0", + "description": "", + "author": "scribear", + "main": "dist/src/index.js", + "type": "module", + "scripts": { + "build": "tsc --build", + "dev": "tsc --build --watch", + "format": "prettier --check ./src ", + "format:fix": "prettier --write ./src", + "lint": "eslint ./src", + "lint:fix": "eslint ./src --fix" + }, + "dependencies": { + "typebox": "1.0.41" + } +} diff --git a/libs/session-manager-schema/src/calculator/compute_binomial.schema.ts b/libs/session-manager-schema/src/calculator/compute_binomial.schema.ts new file mode 100644 index 0000000..645ba59 --- /dev/null +++ b/libs/session-manager-schema/src/calculator/compute_binomial.schema.ts @@ -0,0 +1,41 @@ +import { Type } from 'typebox'; + +import { + type BaseRouteSchema, + SHARED_ERROR_REPLY_SCHEMA, +} from '@scribear/base-schema'; + +const COMPUTE_BINOMIAL_SCHEMA = { + description: 'Computes a binomial', + tags: ['Calculator'], + body: Type.Object( + { + a: Type.Integer({ description: 'First operand' }), + b: Type.Integer({ description: 'Second operand' }), + op: Type.Union([Type.Literal('+'), Type.Literal('-')], { + description: 'Operator', + }), + }, + { + description: 'Represents a single binomial expression', + }, + ), + response: { + 200: Type.Object( + { + result: Type.Integer({ description: 'Result value of computation' }), + reqId: Type.String(), + }, + { description: 'Successful computation response' }, + ), + 400: SHARED_ERROR_REPLY_SCHEMA[400], + 500: SHARED_ERROR_REPLY_SCHEMA[500], + }, +}; + +const COMPUTE_BINOMIAL_ROUTE: BaseRouteSchema = { + method: 'POST', + url: '/calculator/binomial', +}; + +export { COMPUTE_BINOMIAL_SCHEMA, COMPUTE_BINOMIAL_ROUTE }; diff --git a/libs/session-manager-schema/src/calculator/compute_monomial.schema.ts b/libs/session-manager-schema/src/calculator/compute_monomial.schema.ts new file mode 100644 index 0000000..c09834e --- /dev/null +++ b/libs/session-manager-schema/src/calculator/compute_monomial.schema.ts @@ -0,0 +1,40 @@ +import { Type } from 'typebox'; + +import { + type BaseRouteSchema, + SHARED_ERROR_REPLY_SCHEMA, +} from '@scribear/base-schema'; + +const COMPUTE_MONOMIAL_SCHEMA = { + description: 'Computes a monomial', + tags: ['Calculator'], + body: Type.Object( + { + a: Type.Integer({ description: 'Operand' }), + op: Type.Union([Type.Literal('square'), Type.Literal('cube')], { + description: 'Operator', + }), + }, + { + description: 'Represents a single monomial expression', + }, + ), + response: { + 200: Type.Object( + { + result: Type.Integer({ description: 'Result value of computation' }), + reqId: Type.String(), + }, + { description: 'Successful computation response' }, + ), + 400: SHARED_ERROR_REPLY_SCHEMA[400], + 500: SHARED_ERROR_REPLY_SCHEMA[500], + }, +}; + +const COMPUTE_MONOMIAL_ROUTE: BaseRouteSchema = { + method: 'POST', + url: '/calculator/monomial', +}; + +export { COMPUTE_MONOMIAL_SCHEMA, COMPUTE_MONOMIAL_ROUTE }; diff --git a/libs/session-manager-schema/src/healthcheck/healthcheck.schema.ts b/libs/session-manager-schema/src/healthcheck/healthcheck.schema.ts new file mode 100644 index 0000000..e6e9812 --- /dev/null +++ b/libs/session-manager-schema/src/healthcheck/healthcheck.schema.ts @@ -0,0 +1,26 @@ +import { Type } from 'typebox'; + +import { + type BaseRouteSchema, + SHARED_ERROR_REPLY_SCHEMA, +} from '@scribear/base-schema'; + +const HEALTHCHECK_SCHEMA = { + description: 'Probes liveliness of server', + tags: ['Healthcheck'], + response: { + 200: Type.Object( + { reqId: Type.String() }, + { description: 'Healthcheck successful' }, + ), + 400: SHARED_ERROR_REPLY_SCHEMA[400], + 500: SHARED_ERROR_REPLY_SCHEMA[500], + }, +}; + +const HEALTHCHECK_ROUTE: BaseRouteSchema = { + method: 'GET', + url: '/healthcheck', +}; + +export { HEALTHCHECK_SCHEMA, HEALTHCHECK_ROUTE }; diff --git a/libs/session-manager-schema/src/index.ts b/libs/session-manager-schema/src/index.ts new file mode 100644 index 0000000..6eeee24 --- /dev/null +++ b/libs/session-manager-schema/src/index.ts @@ -0,0 +1,21 @@ +import { + COMPUTE_BINOMIAL_ROUTE, + COMPUTE_BINOMIAL_SCHEMA, +} from './calculator/compute_binomial.schema.js'; +import { + COMPUTE_MONOMIAL_ROUTE, + COMPUTE_MONOMIAL_SCHEMA, +} from './calculator/compute_monomial.schema.js'; +import { + HEALTHCHECK_ROUTE, + HEALTHCHECK_SCHEMA, +} from './healthcheck/healthcheck.schema.js'; + +export { + COMPUTE_BINOMIAL_SCHEMA, + COMPUTE_BINOMIAL_ROUTE, + COMPUTE_MONOMIAL_SCHEMA, + COMPUTE_MONOMIAL_ROUTE, + HEALTHCHECK_ROUTE, + HEALTHCHECK_SCHEMA, +}; diff --git a/libs/session-manager-schema/tsconfig.json b/libs/session-manager-schema/tsconfig.json new file mode 100644 index 0000000..0917141 --- /dev/null +++ b/libs/session-manager-schema/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "src", + ], + // Link to dependencies + "references": [ + { + "path": "../base-schema/tsconfig.json" + } + ] +} diff --git a/node-server/.dockerignore b/node-server/.dockerignore deleted file mode 100644 index e908e4e..0000000 --- a/node-server/.dockerignore +++ /dev/null @@ -1,133 +0,0 @@ -build/ -coverage/ - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* \ No newline at end of file diff --git a/node-server/.gitignore b/node-server/.gitignore deleted file mode 100644 index e908e4e..0000000 --- a/node-server/.gitignore +++ /dev/null @@ -1,133 +0,0 @@ -build/ -coverage/ - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* \ No newline at end of file diff --git a/node-server/.npmrc b/node-server/.npmrc deleted file mode 100644 index 449691b..0000000 --- a/node-server/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact=true \ No newline at end of file diff --git a/node-server/.prettierrc.cjs b/node-server/.prettierrc.cjs deleted file mode 100644 index ff15483..0000000 --- a/node-server/.prettierrc.cjs +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - ...require('gts/.prettierrc.json') -} diff --git a/node-server/Dockerfile b/node-server/Dockerfile deleted file mode 100644 index f72b716..0000000 --- a/node-server/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:20 - -WORKDIR /app - -COPY package*.json . - -RUN npm install - -COPY . . - -RUN npm run build - -WORKDIR /app/build/src - -ENV HOST=0.0.0.0 -ENV PORT=80 -ENV KEY_FILEPATH="/app/cert/key.pem" -ENV CERTIFICATE_FILEPATH="/app/cert/cert.pem" - -HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --start-interval=1s --retries=3 CMD curl -f http://localhost:80/api/healthcheck || exit 1 - -CMD ["node", "index.js"] \ No newline at end of file diff --git a/node-server/eslint.config.mjs b/node-server/eslint.config.mjs deleted file mode 100644 index f9eb1f1..0000000 --- a/node-server/eslint.config.mjs +++ /dev/null @@ -1,121 +0,0 @@ -import js from "@eslint/js"; -import path from "node:path"; -import tsParser from "@typescript-eslint/parser"; -import typescriptEslint from "@typescript-eslint/eslint-plugin" -import {FlatCompat} from "@eslint/eslintrc"; -import {fileURLToPath} from "node:url"; -import {fixupConfigRules, fixupPluginRules} from "@eslint/compat"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all -}); - -export default [{ - ignores: [ - ".prettierrc.cjs", - "eslint.config.mjs", - "vite.config.ts", - "coverage/", - "build/", - "drizzle.config.ts", - ], -}, -...fixupConfigRules(compat.extends("plugin:@typescript-eslint/recommended", "./node_modules/gts/")), -{ - plugins: { - "@typescript-eslint": fixupPluginRules(typescriptEslint), - }, - - languageOptions: { - parser: tsParser, - ecmaVersion: 5, - sourceType: "script", - - parserOptions: { - project: true, - }, - }, - - rules: { - "no-throw-literal": "error", - "no-empty-pattern": "off", - "@typescript-eslint/only-throw-error": "error", - "@typescript-eslint/no-unused-expressions": "off", - "@typescript-eslint/no-unused-vars": "warn", - - "n/no-unpublished-import": ["off", { - allowModules: [], - convertPath: null, - }], - - "prettier/prettier": ["error", { - endOfLine: "auto", - printWidth: 120, - }], - - "@typescript-eslint/naming-convention": ["error", - { - selector: [ - "accessor", - "parameter", - "classProperty", - "classMethod", - "variable", - "function", - ], - format: ["camelCase"], - }, { - selector: ["class", "interface"], - format: ["PascalCase"], - }, { - selector: ["enum", "enumMember"], - format: ["PascalCase"], - }, { - selector: ["variable"], - modifiers: ["global", "const"], - format: ["UPPER_CASE", "camelCase"], - }, { - selector: ["accessor", "classProperty", "classMethod"], - modifiers: ["private"], - format: null, - leadingUnderscore: "require", - trailingUnderscore: "forbid", - }, { - selector: ["accessor", "classProperty", "classMethod"], - modifiers: ["protected"], - format: null, - leadingUnderscore: "require", - trailingUnderscore: "forbid", - }, { - selector: ["accessor", "classProperty", "classMethod"], - modifiers: ["public"], - format: null, - leadingUnderscore: "forbid", - trailingUnderscore: "forbid", - }, { - selector: [ - "class", - "enum", - "enumMember", - "function", - "interface", - "objectLiteralMethod", - "objectLiteralProperty", - "typeAlias", - "typeMethod", - "typeParameter", - "typeProperty", - "variable", - ], - - format: null, - leadingUnderscore: "forbid", - trailingUnderscore: "forbid", - } - ], - }, -}]; \ No newline at end of file diff --git a/node-server/package-lock.json b/node-server/package-lock.json deleted file mode 100644 index 88af726..0000000 --- a/node-server/package-lock.json +++ /dev/null @@ -1,6927 +0,0 @@ -{ - "name": "scribear-node-server", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "scribear-node-server", - "version": "0.0.0", - "dependencies": { - "@fastify/cors": "11.0.1", - "@fastify/helmet": "13.0.1", - "@fastify/sensible": "6.0.3", - "@fastify/websocket": "11.0.2", - "@sinclair/typebox": "0.34.15", - "ajv": "8.17.1", - "axios": "1.9.0", - "dotenv": "16.4.7", - "fastify": "5.3.2", - "http": "0.0.1-security", - "pino": "9.6.0", - "tiny-typed-emitter": "2.1.0", - "ws": "8.18.0" - }, - "devDependencies": { - "@eslint/compat": "1.2.6", - "@eslint/eslintrc": "3.2.0", - "@eslint/js": "9.19.0", - "@types/node": "22.13.0", - "@types/ws": "8.5.14", - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@vitest/coverage-istanbul": "3.0.5", - "@vitest/ui": "3.0.5", - "eslint": "9.19.0", - "gts": "6.0.2", - "pino-pretty": "13.0.0", - "ts-patch": "3.3.0", - "tsc-watch": "6.2.1", - "typescript": "5.7.3", - "typescript-transform-paths": "3.5.3", - "vitest": "3.0.5" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", - "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.3.tgz", - "integrity": "sha512-yTmc8J+Sj8yLzwr4PD5Xb/WF3bOYu2C2OoSZPzbuqRm4n98XirsbzaX+GloeO376UnSYIYJ4NCanwV5/ugZkwA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/compat": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.6.tgz", - "integrity": "sha512-k7HNCqApoDHM6XzT30zGoETj+D+uUcZUb+IVAJmar3u6bvHf7hhHJcWx09QHj4/a2qrKZMWU0E16tvkiAdv06Q==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^9.10.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/js": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", - "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.10.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@fastify/ajv-compiler": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz", - "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==", - "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" - } - }, - "node_modules/@fastify/cors": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.0.1.tgz", - "integrity": "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/@fastify/error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", - "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz", - "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==", - "dependencies": { - "fast-json-stringify": "^6.0.0" - } - }, - "node_modules/@fastify/forwarded": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", - "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==" - }, - "node_modules/@fastify/helmet": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-13.0.1.tgz", - "integrity": "sha512-i+ifqazG3d0HwHL3zuZdg6B/WPc9Ee6kVfGpwGho4nxm0UaK1htss0zq+1rVhOoAorZlCgTZ3/i4S58hUGkkoA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "dependencies": { - "fastify-plugin": "^5.0.0", - "helmet": "^8.0.0" - } - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", - "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/@fastify/proxy-addr": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", - "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", - "dependencies": { - "@fastify/forwarded": "^3.0.0", - "ipaddr.js": "^2.1.0" - } - }, - "node_modules/@fastify/sensible": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@fastify/sensible/-/sensible-6.0.3.tgz", - "integrity": "sha512-Iyn8698hp/e5+v8SNBBruTa7UfrMEP52R16dc9jMpqSyEcPsvWFQo+R6WwHCUnJiLIsuci2ZoEZ7ilrSSCPIVg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "dependencies": { - "@lukeed/ms": "^2.0.2", - "dequal": "^2.0.3", - "fastify-plugin": "^5.0.0", - "forwarded": "^0.2.0", - "http-errors": "^2.0.0", - "type-is": "^1.6.18", - "vary": "^1.1.2" - } - }, - "node_modules/@fastify/websocket": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.0.2.tgz", - "integrity": "sha512-1oyJkNSZNJGjo/A5fXvlpEcm1kTBD91nRAN9lA7RNVsVNsyC5DuhOXdNL9/4UawVe7SKvzPT/QVI4RdtE9ylnA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "dependencies": { - "duplexify": "^4.1.3", - "fastify-plugin": "^5.0.0", - "ws": "^8.16.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lukeed/ms": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", - "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.15", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.15.tgz", - "integrity": "sha512-xeIzl3h1Znn9w/LTITqpiwag0gXjA+ldi2ZkXIBxGEppGCW211Tza+eL6D4pKqs10bj5z2umBWk5WL6spQ2OCQ==" - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dev": true - }, - "node_modules/@types/node": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", - "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", - "dev": true, - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, - "node_modules/@types/ws": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", - "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/@vitest/coverage-istanbul": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-3.0.5.tgz", - "integrity": "sha512-yTcIwrpLHOyPP28PXXLRv1NzzKCrqDnmT7oVypTa1Q24P6OwGT4Wi6dXNEaJg33vmrPpoe81f31kwB5MtfM+ow==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.3", - "debug": "^4.4.0", - "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", - "magicast": "^0.3.5", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "3.0.5" - } - }, - "node_modules/@vitest/expect": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", - "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", - "dev": true, - "dependencies": { - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", - "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", - "dev": true, - "dependencies": { - "@vitest/spy": "3.0.5", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", - "dev": true, - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", - "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", - "dev": true, - "dependencies": { - "@vitest/utils": "3.0.5", - "pathe": "^2.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", - "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.0.5", - "magic-string": "^0.30.17", - "pathe": "^2.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", - "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", - "dev": true, - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.0.5.tgz", - "integrity": "sha512-gw2noso6WI+2PeMVCZFntdATS6xl9qhQcbhkPQ9sOmx/Xn0f4Bx4KDSbD90jpJPF0l5wOzSoGCmKyVR3W612mg==", - "dev": true, - "dependencies": { - "@vitest/utils": "3.0.5", - "fflate": "^0.8.2", - "flatted": "^3.3.2", - "pathe": "^2.0.2", - "sirv": "^3.0.0", - "tinyglobby": "^0.2.10", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "3.0.5" - } - }, - "node_modules/@vitest/utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", - "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.0.5", - "loupe": "^3.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/avvio": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", - "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", - "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" - } - }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/builtins": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", - "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001686", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz", - "integrity": "sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", - "dev": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "engines": { - "node": ">=18" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.5.68", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz", - "integrity": "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "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" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", - "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.19.0", - "@eslint/plugin-kit": "^0.2.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-n": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", - "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", - "dev": true, - "dependencies": { - "builtins": "^5.0.1", - "eslint-plugin-es": "^4.1.0", - "eslint-utils": "^3.0.0", - "ignore": "^5.1.1", - "is-core-module": "^2.11.0", - "minimatch": "^3.1.2", - "resolve": "^1.22.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/expect-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", - "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "dev": true - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stringify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz", - "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==", - "dependencies": { - "@fastify/merge-json-schemas": "^0.1.1", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-deep-equal": "^3.1.3", - "fast-uri": "^2.3.0", - "json-schema-ref-resolver": "^1.0.1", - "rfdc": "^1.2.0" - } - }, - "node_modules/fast-json-stringify/node_modules/fast-uri": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", - "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" - }, - "node_modules/fastify": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.3.2.tgz", - "integrity": "sha512-AIPqBgtqBAwkOkrnwesEE+dOyU30dQ4kh7udxeGVR05CRGwubZx+p2H8P0C4cRnQT0+EPK4VGea2DTL2RtWttg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/ajv-compiler": "^4.0.0", - "@fastify/error": "^4.0.0", - "@fastify/fast-json-stringify-compiler": "^5.0.0", - "@fastify/proxy-addr": "^5.0.0", - "abstract-logging": "^2.0.1", - "avvio": "^9.0.0", - "fast-json-stringify": "^6.0.0", - "find-my-way": "^9.0.0", - "light-my-request": "^6.0.0", - "pino": "^9.0.0", - "process-warning": "^5.0.0", - "rfdc": "^1.3.1", - "secure-json-parse": "^4.0.0", - "semver": "^7.6.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/fastify-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", - "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==" - }, - "node_modules/fastify/node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/fastify/node_modules/secure-json-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz", - "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-my-way": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz", - "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/global-prefix": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", - "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", - "dev": true, - "dependencies": { - "ini": "^4.1.3", - "kind-of": "^6.0.3", - "which": "^4.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/global-prefix/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/gts": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/gts/-/gts-6.0.2.tgz", - "integrity": "sha512-lp9+eDzzm6TYqiBpgGY00EInxBHFTJiU5brsVp11qXCJEw7Q6WNNngja0spZeqSFWSquaRuHQUuWxdZLaxnKmw==", - "dev": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "5.62.0", - "@typescript-eslint/parser": "5.62.0", - "chalk": "^4.1.2", - "eslint": "8.57.1", - "eslint-config-prettier": "9.1.0", - "eslint-plugin-n": "15.7.0", - "eslint-plugin-prettier": "5.2.1", - "execa": "^5.0.0", - "inquirer": "^7.3.3", - "json5": "^2.1.3", - "meow": "^9.0.0", - "ncp": "^2.0.0", - "prettier": "3.3.3", - "rimraf": "3.0.2", - "write-file-atomic": "^4.0.0" - }, - "bin": { - "gts": "build/src/cli.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "typescript": ">=5" - } - }, - "node_modules/gts/node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/gts/node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/gts/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/gts/node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/gts/node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/gts/node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/gts/node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/gts/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/gts/node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/gts/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/gts/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/gts/node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/gts/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/gts/node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/gts/node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/gts/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/gts/node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/gts/node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/gts/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gts/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/gts/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/helmet": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.0.0.tgz", - "integrity": "sha512-VyusHLEIIO5mjQPUI1wpOAEu+wl6Q0998jzTxqUYGE45xCIcAxy3MsbEK/yyJUJ3ADeMoB6MornPH6GMWAf+Pw==", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "dev": true - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/http": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", - "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema-ref-resolver": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", - "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/light-my-request": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.3.0.tgz", - "integrity": "sha512-bWTAPJmeWQH5suJNYwG0f5cs0p6ho9e6f1Ppoxv5qMosY+s9Ir2+ZLvvHcgA7VTDop4zl/NCHhOVVqU+kd++Ow==", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "dev": true, - "bin": { - "ncp": "bin/ncp" - } - }, - "node_modules/node-cleanup": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", - "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", - "dev": true - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "dependencies": { - "through": "~2.3" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pino": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", - "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^4.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", - "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", - "dev": true, - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.2", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/process-warning": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", - "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ret": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex2": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz", - "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==", - "dependencies": { - "ret": "~0.5.0" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "dev": true - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", - "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", - "dev": true - }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", - "dev": true - }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", - "dev": true, - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/synckit/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", - "dev": true, - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", - "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", - "dev": true, - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-patch": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ts-patch/-/ts-patch-3.3.0.tgz", - "integrity": "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.2", - "global-prefix": "^4.0.0", - "minimist": "^1.2.8", - "resolve": "^1.22.2", - "semver": "^7.6.3", - "strip-ansi": "^6.0.1" - }, - "bin": { - "ts-patch": "bin/ts-patch.js", - "tspc": "bin/tspc.js" - } - }, - "node_modules/tsc-watch": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-6.2.1.tgz", - "integrity": "sha512-GLwdz5Dy9K3sVm3RzgkLcyDpl5cvU9HEcE1A3gf5rqEwlUe7gDLxNCgcuNEw3zoKOiegMo3LnbF1t6HLqxhrSA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "node-cleanup": "^2.1.2", - "ps-tree": "^1.2.0", - "string-argv": "^0.3.1" - }, - "bin": { - "tsc-watch": "dist/lib/tsc-watch.js" - }, - "engines": { - "node": ">=12.12.0" - }, - "peerDependencies": { - "typescript": "*" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-transform-paths": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript-transform-paths/-/typescript-transform-paths-3.5.3.tgz", - "integrity": "sha512-5y2l2iPKNHKOj08/1i+02+ljBVUhWcXQLXomiOXCmNpiTuSxIkj0dM1LUE7OOAt53+/6KidY+sFTCP781J64Eg==", - "dev": true, - "dependencies": { - "minimatch": "^9.0.5" - }, - "peerDependencies": { - "typescript": ">=3.6.5" - } - }, - "node_modules/typescript-transform-paths/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/typescript-transform-paths/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true - }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", - "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", - "pathe": "^2.0.2", - "vite": "^5.0.0 || ^6.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", - "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", - "dev": true, - "dependencies": { - "@vitest/expect": "3.0.5", - "@vitest/mocker": "3.0.5", - "@vitest/pretty-format": "^3.0.5", - "@vitest/runner": "3.0.5", - "@vitest/snapshot": "3.0.5", - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", - "debug": "^4.4.0", - "expect-type": "^1.1.0", - "magic-string": "^0.30.17", - "pathe": "^2.0.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.5", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.5", - "@vitest/ui": "3.0.5", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/node-server/package.json b/node-server/package.json deleted file mode 100644 index 3c19c93..0000000 --- a/node-server/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "scribear-node-server", - "version": "0.0.0", - "main": "build/src/index.js", - "type": "module", - "scripts": { - "lint": "eslint", - "lint:fix": "eslint --fix", - "clean": "gts clean", - "dev": "tsc-watch --compiler ts-patch/compiler/tsc.js --onSuccess \"node build/src/index.js\" | pino-pretty", - "test:dev": "vitest --ui --coverage", - "test:ci": "vitest run", - "build": "tspc", - "prestart": "npm run build", - "start": "node ./build/src/index.js" - }, - "author": "scribear", - "description": "", - "engines": { - "node": ">=20.0.0" - }, - "devDependencies": { - "@eslint/compat": "1.2.6", - "@eslint/eslintrc": "3.2.0", - "@eslint/js": "9.19.0", - "@types/node": "22.13.0", - "@types/ws": "8.5.14", - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@vitest/coverage-istanbul": "3.0.5", - "@vitest/ui": "3.0.5", - "eslint": "9.19.0", - "gts": "6.0.2", - "pino-pretty": "13.0.0", - "ts-patch": "3.3.0", - "tsc-watch": "6.2.1", - "typescript": "5.7.3", - "typescript-transform-paths": "3.5.3", - "vitest": "3.0.5" - }, - "dependencies": { - "@fastify/cors": "11.0.1", - "@fastify/helmet": "13.0.1", - "@fastify/sensible": "6.0.3", - "@fastify/websocket": "11.0.2", - "@sinclair/typebox": "0.34.15", - "ajv": "8.17.1", - "axios": "1.9.0", - "dotenv": "16.4.7", - "fastify": "5.3.2", - "http": "0.0.1-security", - "pino": "9.6.0", - "tiny-typed-emitter": "2.1.0", - "ws": "8.18.0" - } -} diff --git a/node-server/src/index.ts b/node-server/src/index.ts deleted file mode 100644 index 7107747..0000000 --- a/node-server/src/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import loadConfig from './shared/config/load_config.js'; -import createServer from './server/create_server.js'; -import createLogger from './shared/logger/logger.js'; - -async function init() { - const config = loadConfig(); - const logger = createLogger(config); - if (config.isDevelopment) logger.debug({msg: 'App configuration', config}); - const server = createServer(config, logger); - - process.on('uncaughtException', err => { - logger.fatal({msg: 'Uncaught exception', err}); - throw err; // terminate on uncaught errors - }); - - process.on('unhandledRejection', reason => { - const err = new Error(`Unhanded rejection. Reason: ${reason}`); - logger.fatal({msg: 'Unhandled rejection', err}); - throw err; // terminate on uncaught rejection - }); - - try { - await server.listen({port: config.server.port, host: config.server.host}); - } catch (err) { - logger.fatal({msg: 'Failed to start webserver', err}); - throw err; // terminate if fails to start - } - - // Close connection and server on SIGTERM/SIGINT for a graceful exit - process.on('SIGTERM', async () => { - logger.info('SIGTERM received, exiting gracefully.'); - await server.close(); - // eslint-disable-next-line n/no-process-exit - process.exit(); - }); - process.on('SIGINT', async () => { - logger.info('SIGINT received, exiting gracefully.'); - await server.close(); - // eslint-disable-next-line n/no-process-exit - process.exit(); - }); -} - -await init(); diff --git a/node-server/src/server/create_server.ts b/node-server/src/server/create_server.ts deleted file mode 100644 index 9b345de..0000000 --- a/node-server/src/server/create_server.ts +++ /dev/null @@ -1,62 +0,0 @@ -import Fastify, {type FastifyInstance} from 'fastify'; -import fastifyCors from '@fastify/cors'; -import type {ConfigType} from '../shared/config/config_schema.js'; -import type {Logger} from '../shared/logger/logger.js'; -import TranscriptionEngine from './services/transcription_engine.js'; -import fastifyWebsocket from '@fastify/websocket'; -import websocketHandler from './routes/websocket_handler.js'; -import fastifyHelmet from '@fastify/helmet'; -import fastifySensible from '@fastify/sensible'; -import TokenService from './services/token_service.js'; -import accessTokenHandler from './routes/session_auth_handler.js'; -import healthcheckHandler from './routes/healthcheck_handler.js'; -import AuthenticationService from './services/authentication_service.js'; - -declare module 'fastify' { - export interface FastifyInstance { - config: ConfigType; - authenticationService: AuthenticationService; - transcriptionEngine: TranscriptionEngine; - tokenService: TokenService; - } - - export interface FastifyRequest { - authorizationExpiryTimeout?: number; - } -} - -/** - * Creates fastify server and loads all plugins, hooks, and modules from corresponding folders - * @returns created fastify server - */ -export default function createServer(config: ConfigType, logger: Logger) { - const fastify = Fastify({loggerInstance: logger}) as unknown as FastifyInstance; - - fastify.register(fastifyWebsocket); - - fastify.register(fastifyCors, { - origin: config.server.corsOrigin, - methods: ['GET', 'POST'], - }); - - // Security and sensible defaults - fastify.register(fastifyHelmet); - fastify.register(fastifySensible); - - // Make configuration and transcription engine avaiable on fastify instance (dependency injection) - fastify.decorate('config', config); - fastify.decorate('transcriptionEngine', new TranscriptionEngine(config, logger)); - fastify.decorate('tokenService', new TokenService(config, logger)); - fastify.decorate('authenticationService', new AuthenticationService(config, fastify.tokenService)); - - fastify.register( - async api => { - api.register(websocketHandler); - api.register(accessTokenHandler); - api.register(healthcheckHandler); - }, - {prefix: '/api'}, - ); - - return fastify; -} diff --git a/node-server/src/server/hooks/create_authorize_hook.test.ts b/node-server/src/server/hooks/create_authorize_hook.test.ts deleted file mode 100644 index 27da14b..0000000 --- a/node-server/src/server/hooks/create_authorize_hook.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Fastify from 'fastify'; -import {describe, expect, vi} from 'vitest'; -import createAuthorizeHook from './create_authorize_hook.js'; -import type AuthenticationService from '@server/services/authentication_service.js'; -import {Identities} from '@server/services/authentication_service.js'; - -describe('createAuthorizeHook', it => { - function setupTest(fakeAuthenticationService: AuthenticationService, allowedIdentities: Array) { - const fastify = Fastify(); - const reqHandlerSpy = vi.fn((req, reply) => reply.code(200).send('hi')); - fastify.post( - '/test', - {preHandler: createAuthorizeHook(fakeAuthenticationService, allowedIdentities)}, - reqHandlerSpy, - ); - - return {fastify, reqHandlerSpy}; - } - - it('calls AuthenticationService with provided tokens and identities', async () => { - const fakeAuthenticationService = { - authorizeTokens: vi.fn(() => { - return {authorized: true}; - }), - } as unknown as AuthenticationService; - const identities = [Identities.AccessToken, Identities.SourceToken]; - - const {fastify} = setupTest(fakeAuthenticationService, identities); - await fastify.inject({ - method: 'POST', - path: '/test', - body: { - accessToken: 'accessToken', - sessionToken: 'sessionToken', - sourceToken: 'sourceToken', - }, - }); - - expect(fakeAuthenticationService.authorizeTokens).toHaveBeenCalledExactlyOnceWith( - 'accessToken', - 'sessionToken', - 'sourceToken', - identities, - fastify.log, - ); - }); - - it('accepts request when AuthenticationService accepts tokens', async () => { - const fakeAuthenticationService = { - authorizeTokens: vi.fn(() => { - return {authorized: true}; - }), - } as unknown as AuthenticationService; - - const {fastify, reqHandlerSpy} = setupTest(fakeAuthenticationService, []); - await fastify.inject({ - method: 'POST', - path: '/test', - body: { - accessToken: 'accessToken', - sessionToken: 'sessionToken', - sourceToken: 'sourceToken', - }, - }); - - expect(reqHandlerSpy).toHaveBeenCalledOnce(); - }); - - it('rejects request when AuthenticationService rejects tokens', async () => { - const fakeAuthenticationService = { - authorizeTokens: vi.fn(() => { - return {authorized: false}; - }), - } as unknown as AuthenticationService; - - const {fastify, reqHandlerSpy} = setupTest(fakeAuthenticationService, []); - await fastify.inject({ - method: 'POST', - path: '/test', - body: { - accessToken: 'accessToken', - sessionToken: 'sessionToken', - sourceToken: 'sourceToken', - }, - }); - - expect(reqHandlerSpy).not.toHaveBeenCalled(); - }); - - it('decorates request with authorizationExpiryTimeout when provided', async () => { - const fakeAuthenticationService = { - authorizeTokens: vi.fn(() => { - return {authorized: true, expiresInMs: 1000}; - }), - } as unknown as AuthenticationService; - - const {fastify, reqHandlerSpy} = setupTest(fakeAuthenticationService, []); - await fastify.inject({ - method: 'POST', - path: '/test', - body: { - accessToken: 'accessToken', - sessionToken: 'sessionToken', - sourceToken: 'sourceToken', - }, - }); - - expect(reqHandlerSpy.mock.calls[0][0].authorizationExpiryTimeout).toBe(1000); - }); -}); diff --git a/node-server/src/server/hooks/create_authorize_hook.ts b/node-server/src/server/hooks/create_authorize_hook.ts deleted file mode 100644 index b4b7529..0000000 --- a/node-server/src/server/hooks/create_authorize_hook.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type {Identities} from '@server/services/authentication_service.js'; -import type AuthenticationService from '@server/services/authentication_service.js'; -import type {Logger} from '@shared/logger/logger.js'; -import type {DoneFuncWithErrOrRes, FastifyReply, FastifyRequest} from 'fastify'; - -/** - * Creates a fastify preHandler hook to handle authorizing requests - * Checks request query string for sessionToken or sourceToken and checks if they are valid - - * @param authenticationService authenticationService instance - * @param allowedIdentities array of identities that should be accepted - * @returns fastify preHandler hook - */ -export default function createAuthorizeHook( - authenticationService: AuthenticationService, - allowedIdentities: Array, -) { - return ( - request: FastifyRequest<{ - Body?: {accessToken?: string; sessionToken?: string; sourceToken?: string}; - }>, - reply: FastifyReply, - done: DoneFuncWithErrOrRes, - ) => { - const accessToken = request.body?.accessToken; - const sessionToken = request.body?.sessionToken; - const sourceToken = request.body?.sourceToken; - - const {authorized, expiresInMs} = authenticationService.authorizeTokens( - accessToken, - sessionToken, - sourceToken, - allowedIdentities, - request.log as Logger, - ); - - if (authorized) { - request.authorizationExpiryTimeout = expiresInMs; - return done(); - } - - reply.code(403); - done(new Error('Unauthorized')); - }; -} diff --git a/node-server/src/server/routes/healthcheck_handler.ts b/node-server/src/server/routes/healthcheck_handler.ts deleted file mode 100644 index 0e4bd8e..0000000 --- a/node-server/src/server/routes/healthcheck_handler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {FastifyInstance} from 'fastify'; - -/** - * Registers endpoint for checking if server is alive - * @param fastify fastify webserver instance - */ -export default function healthcheckHandler(fastify: FastifyInstance) { - fastify.get('/healthcheck', (req, reply) => { - return reply.code(200).send('ok'); - }); -} diff --git a/node-server/src/server/routes/session_auth_handler.ts b/node-server/src/server/routes/session_auth_handler.ts deleted file mode 100644 index 2dba599..0000000 --- a/node-server/src/server/routes/session_auth_handler.ts +++ /dev/null @@ -1,38 +0,0 @@ -import createAuthorizeHook from '@server/hooks/create_authorize_hook.js'; -import {Identities} from '@server/services/authentication_service.js'; -import {FastifyInstance} from 'fastify'; - -/** - * Registers access token and session token endpoints for managing authentication - * @param fastify fastify webserver instance - */ -export default function sessionAuthHandler(fastify: FastifyInstance) { - fastify.post( - '/accessToken', - {preHandler: createAuthorizeHook(fastify.authenticationService, [Identities.SourceToken])}, - (request, reply) => { - const {accessToken, expires} = fastify.tokenService.getAccessToken(); - return reply.send({ - accessToken, - serverAddress: fastify.config.server.serverAddress, - expires: expires.toISOString(), - }); - }, - ); - - fastify.post( - '/startSession', - {preHandler: createAuthorizeHook(fastify.authenticationService, [Identities.AccessToken])}, - (request, reply) => { - if (typeof request.body !== 'object') return reply.code(400).send(); - const {accessToken} = request.body as {accessToken?: string}; - - if (!fastify.tokenService.accessTokenIsValid(accessToken)) { - return reply.code(401).send(); - } - - const {sessionToken, expires} = fastify.tokenService.createSessionToken(); - return reply.send({sessionToken, expires: expires.toISOString()}); - }, - ); -} diff --git a/node-server/src/server/routes/websocket_handler.ts b/node-server/src/server/routes/websocket_handler.ts deleted file mode 100644 index b4f3a77..0000000 --- a/node-server/src/server/routes/websocket_handler.ts +++ /dev/null @@ -1,175 +0,0 @@ -import AuthenticationService, {Identities} from '@server/services/authentication_service.js'; -import type {BackendTranscriptBlock} from '@server/services/transcription_engine.js'; -import type {Logger} from '@shared/logger/logger.js'; -import {FastifyInstance, type FastifyBaseLogger} from 'fastify'; -import WebSocket from 'ws'; - -/** - * Helper function for determining authorization for a websocket - * @param ws websocket to register - * @param authenticationService authenticationService instance - * @param allowedIdentities array of identities that should be accepted - * @param log logger - * @returns true if authorized, false otherwise - */ -async function authorizeWebsocket( - ws: WebSocket, - authenticationService: AuthenticationService, - allowedIdentities: Array, - log: FastifyBaseLogger, -): Promise { - const jsonMessage = new Promise(resolve => { - ws.once('message', data => { - try { - resolve(JSON.parse(data.toString())); - } catch (err) { - log.error({msg: 'Invalid authorization message received from client', err}); - } - }); - }); - - const timeout = new Promise(resolve => setTimeout(() => resolve(false), 5_000)); - - const result = await Promise.any([jsonMessage, timeout]); - if (result === false) { - log.info({msg: 'Authorization timed out'}); - return false; - } - log.debug({msg: 'Received authorization message from client', message: result}); - - if (typeof result !== 'object' || result === null) { - log.error({msg: 'Invalid authorization message received from client: Not an object'}); - return false; - } - - const sourceToken = (result as {sourceToken: string})['sourceToken']; - const sessionToken = (result as {sessionToken: string})['sessionToken']; - const {authorized} = authenticationService.authorizeTokens( - undefined, - sessionToken, - sourceToken, - allowedIdentities, - log as Logger, - ); - return authorized; -} - -/** - * Register a websocket that listens for transcription events and forwards them - * Closes websocket when session becomes invalid - * @param fastify fastify webserver instance - * @param ws websocket to register - * @param log logger - */ -function registerSink(fastify: FastifyInstance, ws: WebSocket, log: FastifyBaseLogger) { - log.debug('Registering websocket as sink'); - const onTranscription = (block: BackendTranscriptBlock) => { - try { - log.trace({msg: 'Forwarding transcription block to client', block}); - ws.send(JSON.stringify(block)); - } catch (err) { - log.error({msg: 'Error sending transcription to sink', err}); - } - }; - - fastify.transcriptionEngine.on('transcription', onTranscription); - - ws.on('close', () => { - log.trace('Websocket closed, removing transcription event listener'); - fastify.transcriptionEngine.removeListener('transcription', onTranscription); - }); -} - -/** - * Register a websocket that sends audio to be transcribed - * Will throw error if a second websocket is registered before first has closed - * @param fastify fastify webserver instance - * @param ws websocket to register - * @param log logger - */ -function registerSource(fastify: FastifyInstance, ws: WebSocket, log: FastifyBaseLogger) { - fastify.transcriptionEngine.connectWhisperService(); - - log.debug('Registering websocket as source'); - const onSourceMessage = (data: JSON) => { - try { - log.trace({msg: 'Forwarding source message to client', data}); - ws.send(JSON.stringify(data)); - } catch (err) { - log.error({msg: 'Error sending source message to source', err}); - } - }; - - ws.on('message', (data, isBinary) => { - log.trace({msg: 'Forwarding message for transcription', data}); - fastify.transcriptionEngine.forwardMessage(data, isBinary); - }); - - fastify.transcriptionEngine.on('sourceMessage', onSourceMessage); - ws.on('close', () => { - log.trace('Websocket closed, removing sourceMessage event listener'); - fastify.transcriptionEngine.removeListener('sourceMessage', onSourceMessage); - fastify.transcriptionEngine.disconnectWhisperService(); - }); -} - -/** - * Registers websocket endpoints for transcription source and sinks with fastify webserver - * @param fastify - */ -export default function websocketHandler(fastify: FastifyInstance) { - fastify.get('/sourcesink', {websocket: true}, async (ws, req) => { - if (!(await authorizeWebsocket(ws, fastify.authenticationService, [Identities.SourceToken], fastify.log))) { - return ws.close(); - } - - registerSink(fastify, ws, req.log); - registerSource(fastify, ws, req.log); - - ws.on('close', code => { - req.log.info({msg: 'Websocket closed', code}); - }); - }); - - fastify.get('/source', {websocket: true}, async (ws, req) => { - if (!(await authorizeWebsocket(ws, fastify.authenticationService, [Identities.SourceToken], fastify.log))) { - return ws.close(); - } - - registerSource(fastify, ws, req.log); - - ws.on('close', code => { - req.log.info({msg: 'Websocket closed', code}); - }); - }); - - fastify.get('/sink', {websocket: true}, async (ws, req) => { - if ( - !(await authorizeWebsocket( - ws, - fastify.authenticationService, - [Identities.SourceToken, Identities.SessionToken], - fastify.log, - )) - ) { - return ws.close(); - } - - registerSink(fastify, ws, req.log); - - // Close websocket when session expires - let expirationTimeout: NodeJS.Timeout | undefined; - if (req.authorizationExpiryTimeout) { - fastify.log.debug({msg: 'Setting session expiration timeout', timeout: req.authorizationExpiryTimeout}); - expirationTimeout = setTimeout(() => { - fastify.log.info('Session token expired, closing socket.'); - ws.close(3000); - }, req.authorizationExpiryTimeout); - } - - ws.on('close', code => { - clearTimeout(expirationTimeout); - req.log.info({msg: 'Websocket closed', code}); - }); - }); -} diff --git a/node-server/src/server/services/authentication_service.test.ts b/node-server/src/server/services/authentication_service.test.ts deleted file mode 100644 index 1423327..0000000 --- a/node-server/src/server/services/authentication_service.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import {describe, expect, vi} from 'vitest'; -import type TokenService from './token_service.js'; -import type {ConfigType} from '@shared/config/config_schema.js'; -import AuthenticationService, {Identities} from './authentication_service.js'; -import formatTestNames from '@test/utils/format_test_names.js'; -import fakeLogger from '@test/fakes/fake_logger.js'; - -describe('createAuthorizeHook', it => { - const validAccessToken = 'valid-access-token'; - const validSessionToken = 'valid-session-token'; - const validSourceToken = 'valid-source-token'; - - const fakeConfig = { - auth: { - required: true, - sourceToken: validSourceToken, - }, - } as ConfigType; - - const fakeTokenService = { - accessTokenIsValid: vi.fn(token => { - return token === validAccessToken; - }), - sessionTokenIsValid: vi.fn(token => { - return token === validSessionToken; - }), - getSessionTokenExpiry: vi.fn(() => { - return new Date(Date.now() + 10_000); - }), - } as unknown as TokenService; - - const validCases = formatTestNames([ - { - name: 'Only AccessToken allowed, only AccessToken provided', - identities: [Identities.AccessToken], - tokens: {accessToken: validAccessToken, sessionToken: undefined, sourceToken: undefined}, - }, - { - name: 'Only SessionToken allowed, only SessionToken provided', - identities: [Identities.SessionToken], - tokens: {accessToken: undefined, sessionToken: validSessionToken, sourceToken: undefined}, - }, - { - name: 'Only SourceToken allowed, only SourceToken provided', - identities: [Identities.SourceToken], - tokens: {accessToken: undefined, sessionToken: undefined, sourceToken: validSourceToken}, - }, - { - name: 'All tokens allowed, AccessToken provided', - identities: [Identities.AccessToken, Identities.SessionToken, Identities.SourceToken], - tokens: {accessToken: validAccessToken, sessionToken: undefined, sourceToken: undefined}, - }, - { - name: 'All tokens allowed, SessionToken provided', - identities: [Identities.AccessToken, Identities.SessionToken, Identities.SourceToken], - tokens: {accessToken: undefined, sessionToken: validSessionToken, sourceToken: undefined}, - }, - { - name: 'All tokens allowed, SourceToken provided', - identities: [Identities.AccessToken, Identities.SessionToken, Identities.SourceToken], - tokens: {accessToken: undefined, sessionToken: undefined, sourceToken: validSourceToken}, - }, - ]); - - const invalidCases = formatTestNames([ - { - name: 'Only AccessToken allowed, no tokens provided', - identities: [Identities.AccessToken], - tokens: {accessToken: undefined, sessionToken: undefined, sourceToken: undefined}, - }, - { - name: 'Only SessionToken allowed, no tokens provided', - identities: [Identities.SessionToken], - tokens: {accessToken: undefined, sessionToken: undefined, sourceToken: undefined}, - }, - { - name: 'Only SourceToken allowed, no tokens provided', - identities: [Identities.SourceToken], - tokens: {accessToken: undefined, sessionToken: undefined, sourceToken: undefined}, - }, - { - name: 'All tokens allowed, no tokens provided', - identities: [Identities.AccessToken, Identities.SessionToken, Identities.SourceToken], - tokens: {accessToken: undefined, sessionToken: undefined, sourceToken: undefined}, - }, - { - name: 'Only AccessToken allowed, SessionToken provided', - identities: [Identities.AccessToken], - tokens: {accessToken: undefined, sessionToken: validSessionToken, sourceToken: undefined}, - }, - { - name: 'Only SessionToken allowed, SourceToken provided', - identities: [Identities.SessionToken], - tokens: {accessToken: undefined, sessionToken: undefined, sourceToken: validSourceToken}, - }, - { - name: 'Only SourceToken allowed, AccessToken provided', - identities: [Identities.SessionToken], - tokens: {accessToken: validAccessToken, sessionToken: undefined, sourceToken: undefined}, - }, - ]); - - it.for(validCases)('approves authorization whe: %s', ([, {identities, tokens}]) => { - const {accessToken, sessionToken, sourceToken} = tokens; - const as = new AuthenticationService(fakeConfig, fakeTokenService); - - const {authorized} = as.authorizeTokens(accessToken, sessionToken, sourceToken, identities, fakeLogger()); - - expect(authorized).toBe(true); - }); - - it.for(invalidCases)('rejects authentication when: %s', ([, {identities, tokens}]) => { - const {accessToken, sessionToken, sourceToken} = tokens; - const as = new AuthenticationService(fakeConfig, fakeTokenService); - - const {authorized} = as.authorizeTokens(accessToken, sessionToken, sourceToken, identities, fakeLogger()); - - expect(authorized).toBe(false); - }); - - it('accepts authentication when authentication is turned off', () => { - const as = new AuthenticationService({auth: {required: false}} as ConfigType, fakeTokenService); - - const {authorized} = as.authorizeTokens(undefined, undefined, undefined, [], fakeLogger()); - - expect(authorized).toBe(true); - }); - - it('returns expiration timeout for session token', () => { - const as = new AuthenticationService(fakeConfig, fakeTokenService); - - const {expiresInMs} = as.authorizeTokens( - undefined, - validSessionToken, - undefined, - [Identities.SessionToken], - fakeLogger(), - ); - - expect(typeof expiresInMs).toBe('number'); - expect(Math.abs((expiresInMs ? expiresInMs : 0) - 10_000)).toBeLessThan(100); - }); -}); diff --git a/node-server/src/server/services/authentication_service.ts b/node-server/src/server/services/authentication_service.ts deleted file mode 100644 index 826886e..0000000 --- a/node-server/src/server/services/authentication_service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type {ConfigType} from '@shared/config/config_schema.js'; -import type {Logger} from '@shared/logger/logger.js'; -import type TokenService from './token_service.js'; - -export enum Identities { - SourceToken, - AccessToken, - SessionToken, -} - -export default class AuthenticationService { - constructor( - private _config: ConfigType, - private _tokenService: TokenService, - ) {} - - /** - * Checks if provided tokens correspond to an allowed identity - * Token must be valid in order for identity to be considered - * If the parsed identity is in allowedIdentities, request is authorized, otherwise it is rejected - * @param accessToken - * @param sessionToken - * @param sourceToken - * @param allowedIdentities - * @param log - * @returns object with authorized property indicating result - * if identity was a SourceToken, expiresInMs property is also provided - */ - authorizeTokens( - accessToken: string | undefined, - sessionToken: string | undefined, - sourceToken: string | undefined, - allowedIdentities: Array, - log: Logger, - ): {authorized: boolean; expiresInMs?: number} { - if (!this._config.auth.required) return {authorized: true}; - - if (allowedIdentities.includes(Identities.AccessToken) && this._tokenService.accessTokenIsValid(accessToken)) { - log.debug('Request authorized via access token'); - return {authorized: true}; - } - - if (allowedIdentities.includes(Identities.SessionToken) && this._tokenService.sessionTokenIsValid(sessionToken)) { - const expiration = this._tokenService.getSessionTokenExpiry(sessionToken); - if (expiration) { - log.debug('Request authorized via session token'); - const authorizationExpiryTimeout = expiration.getTime() - new Date().getTime(); - return {authorized: true, expiresInMs: authorizationExpiryTimeout}; - } - } - - if (allowedIdentities.includes(Identities.SourceToken) && sourceToken === this._config.auth.sourceToken) { - log.debug('Request authorized via source token'); - return {authorized: true}; - } - return {authorized: false}; - } -} diff --git a/node-server/src/server/services/token_service.test.ts b/node-server/src/server/services/token_service.test.ts deleted file mode 100644 index 1f2c8ea..0000000 --- a/node-server/src/server/services/token_service.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import {describe, expect, vi} from 'vitest'; -import RequestAuthorizer from './token_service.js'; -import fakeLogger from '../../../test/fakes/fake_logger.js'; -import type {ConfigType} from '@shared/config/config_schema.js'; - -const MAX_TIMESTAMP = 8640000000000000; - -describe('Token service', () => { - function setupTest() { - vi.useFakeTimers(); - vi.setSystemTime(new Date(0)); - const ra = new RequestAuthorizer( - { - auth: { - required: true, - accessTokenBytes: 8, - accessTokenRefreshIntervalSec: 1, - accessTokenValidPeriodSec: 10, - sessionTokenBytes: 32, - sessionLengthSec: 30, - }, - } as ConfigType, - fakeLogger(), - ); - return ra; - } - - describe('Access tokens', it => { - it('generates new access tokens regularly', () => { - const ra = setupTest(); - - const {accessToken: token0} = ra.getAccessToken(); - vi.advanceTimersByTime(2000); - const {accessToken: token1} = ra.getAccessToken(); - - expect(token0).not.toEqual(token1); - }); - - it('generates new access tokens with correct expiration', () => { - const ra = setupTest(); - - const {expires: expires0} = ra.getAccessToken(); - vi.advanceTimersByTime(2000); - const {expires: expires1} = ra.getAccessToken(); - - expect(expires0).toEqual(new Date(10_000)); - expect(expires1).toEqual(new Date(10_000 + 2_000)); - }); - - it('accepts valid access token', () => { - const ra = setupTest(); - - const {accessToken} = ra.getAccessToken(); - - expect(ra.accessTokenIsValid(accessToken)).toBeTruthy(); - }); - - it('rejects expired access tokens', () => { - const ra = setupTest(); - - const {accessToken} = ra.getAccessToken(); - vi.advanceTimersByTime(10_000); - - expect(ra.accessTokenIsValid(accessToken)).not.toBeTruthy(); - }); - - it('rejects invalid access tokens', () => { - const ra = setupTest(); - - expect(ra.accessTokenIsValid('invalid')).not.toBeTruthy(); - }); - }); - - describe('Session tokens', it => { - it('generates new unique session tokens', () => { - const ra = setupTest(); - - const {sessionToken: token0} = ra.createSessionToken(); - vi.advanceTimersByTime(2000); - const {sessionToken: token1} = ra.createSessionToken(); - - expect(token0).not.toEqual(token1); - }); - - it('returns correct session token expiration', () => { - const ra = setupTest(); - - const {sessionToken} = ra.createSessionToken(); - - const expires = ra.getSessionTokenExpiry(sessionToken); - expect(expires).toEqual(new Date(30_000)); - }); - - it('returns undefined session token expiration for invalid session tokens', () => { - const ra = setupTest(); - - const {sessionToken} = ra.createSessionToken(); - - const expires = ra.getSessionTokenExpiry(sessionToken + 'invalid token'); - expect(expires).toBeUndefined(); - }); - - it('generates new session tokens with correct expiration', () => { - const ra = setupTest(); - - const {expires: expires0} = ra.createSessionToken(); - vi.advanceTimersByTime(2000); - const {expires: expires1} = ra.createSessionToken(); - - expect(expires0).toEqual(new Date(30_000)); - expect(expires1).toEqual(new Date(30_000 + 2_000)); - }); - - it('accepts valid session token', () => { - const ra = setupTest(); - - const {sessionToken} = ra.createSessionToken(); - - expect(ra.sessionTokenIsValid(sessionToken)).toBeTruthy(); - }); - - it('rejects expired session tokens', () => { - const ra = setupTest(); - - const {sessionToken} = ra.createSessionToken(); - vi.advanceTimersByTime(60_000); - - expect(ra.sessionTokenIsValid(sessionToken)).not.toBeTruthy(); - }); - - it('rejects invalid session tokens', () => { - const ra = setupTest(); - - expect(ra.sessionTokenIsValid('invalid')).not.toBeTruthy(); - }); - }); - - describe('Authorization override', it => { - it('generates access tokens with max expiry', () => { - const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger()); - - const {expires} = ra.getAccessToken(); - - expect(expires).toEqual(new Date(MAX_TIMESTAMP)); - }); - - it('always accepts access tokens', () => { - const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger()); - - const {accessToken} = ra.getAccessToken(); - - expect(ra.accessTokenIsValid(accessToken)).toBeTruthy(); - expect(ra.accessTokenIsValid('invalid')).toBeTruthy(); - }); - - it('generates session tokens with max expiry', () => { - const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger()); - - const {sessionToken} = ra.createSessionToken(); - - expect(ra.getSessionTokenExpiry(sessionToken)).toEqual(new Date(MAX_TIMESTAMP)); - }); - - it('always accepts session tokens', () => { - const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger()); - - const {sessionToken} = ra.createSessionToken(); - - expect(ra.accessTokenIsValid(sessionToken)).toBeTruthy(); - expect(ra.sessionTokenIsValid('invalid')).toBeTruthy(); - }); - }); -}); diff --git a/node-server/src/server/services/token_service.ts b/node-server/src/server/services/token_service.ts deleted file mode 100644 index 47aaf70..0000000 --- a/node-server/src/server/services/token_service.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type {ConfigType} from '@shared/config/config_schema.js'; -import type {Logger} from '@shared/logger/logger.js'; -import crypto from 'node:crypto'; - -const MAX_TIMESTAMP = 8640000000000000; - -export default class TokenService { - private _log: Logger; - private _validAccessTokens: {[key: string]: Date} = {}; - private _validSessionTokens: {[key: string]: Date} = {}; - private _currentAccessToken = ' '; - - constructor( - private _config: ConfigType, - log: Logger, - ) { - this._log = log.child({service: 'TokenService'}); - - this._updateAccessTokens(); - - if (this._config.auth.required) { - setInterval(() => { - this._updateAccessTokens(); - }, this._config.auth.accessTokenRefreshIntervalSec * 1000); - } - } - - /** - * Refreshes the currently active token and deletes old access tokens - * Updates this._currentAccessToken, this._validAccessTokens, and this._validSessionTokens - */ - private _updateAccessTokens() { - if (!this._config.auth.required) return; - this._log.debug('Updating access tokens'); - - this._currentAccessToken = crypto.randomBytes(this._config.auth.accessTokenBytes).toString('base64url'); - const expiry = new Date(Date.now() + this._config.auth.accessTokenValidPeriodSec * 1000); - this._validAccessTokens[this._currentAccessToken] = expiry; - this._log.trace({msg: 'Created new access token', accessToken: this._currentAccessToken, expiry}); - - // Remove invalid tokens - const currTime = new Date(Date.now()); - for (const token in this._validAccessTokens) { - if (this._validAccessTokens[token] <= currTime) { - delete this._validAccessTokens[token]; - } - } - for (const token in this._validSessionTokens) { - if (this._validSessionTokens[token] <= currTime) { - delete this._validSessionTokens[token]; - } - } - } - - /** - * Computes the expiration date of a new session token - * @returns expiry date - */ - private _computeNewSessionExpiry() { - if (!this._config.auth.required) { - return new Date(MAX_TIMESTAMP); - } - return new Date(Date.now() + this._config.auth.sessionLengthSec * 1000); - } - - /** - * Gets the currently active access token and the expiration of said access token - * @returns object containg access token and expiry date - */ - getAccessToken() { - if (!this._config.auth.required) { - return { - accessToken: this._currentAccessToken, - expires: new Date(MAX_TIMESTAMP), - }; - } - - return { - accessToken: this._currentAccessToken, - expires: this._validAccessTokens[this._currentAccessToken], - }; - } - - /** - * Checks if a given access token is valid - * If authentication is disabled, this function always returns true - * @param accessToken access token to check - * @returns true if valid, false otherwise - */ - accessTokenIsValid(accessToken: string | undefined) { - if (!this._config.auth.required) return true; - - const valid = - typeof accessToken === 'string' && - accessToken in this._validAccessTokens && - this._validAccessTokens[accessToken] > new Date(Date.now()); - - this._log.trace({msg: 'Checking access token is valid', accessToken, valid}); - - return valid; - } - - /** - * Fetches expiration time of given session token - * @param sessionToken session token to fetch - * @returns expiry date or undefined if no valid session token found - */ - getSessionTokenExpiry(sessionToken: string | undefined) { - if (!this._config.auth.required) return new Date(MAX_TIMESTAMP); - - if (typeof sessionToken !== 'string' || !(sessionToken in this._validSessionTokens)) return undefined; - return this._validSessionTokens[sessionToken]; - } - - /** - * Checks if a given session token is valid - * If authentication is disabled, this function always returns true - * @param sessionToken session token to check - * @returns true if valid, false otherwise - */ - sessionTokenIsValid(sessionToken: string | undefined) { - if (!this._config.auth.required) return true; - - const valid = - typeof sessionToken === 'string' && - sessionToken in this._validSessionTokens && - this._validSessionTokens[sessionToken] > new Date(Date.now()); - - this._log.trace({msg: 'Checking session token is valid', sessionToken, valid}); - - return valid; - } - - /** - * Creates a new session token with configured expiry - * @returns created session token - */ - createSessionToken() { - if (!this._config.auth.required) { - return {sessionToken: ' ', expires: this._computeNewSessionExpiry()}; - } - let sessionToken = crypto.randomBytes(this._config.auth.sessionTokenBytes).toString('base64url'); - while (sessionToken in this._validSessionTokens) { - sessionToken = crypto.randomBytes(this._config.auth.sessionTokenBytes).toString('base64url'); - } - - const expires = this._computeNewSessionExpiry(); - this._validSessionTokens[sessionToken] = expires; - - this._log.trace({msg: 'Creating new session token', sessionToken, expires}); - - return {sessionToken, expires}; - } -} diff --git a/node-server/src/server/services/transcription_engine.test.ts b/node-server/src/server/services/transcription_engine.test.ts deleted file mode 100644 index 044362c..0000000 --- a/node-server/src/server/services/transcription_engine.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import {describe, expect} from 'vitest'; -import {WebSocketServer, WebSocket} from 'ws'; -import http from 'http'; -import type {AddressInfo} from 'net'; -import TranscriptionEngine, {BackendTranscriptBlockType, type BackendTranscriptBlock} from './transcription_engine.js'; -import fakeLogger from '../../../test/fakes/fake_logger.js'; -import type {ConfigType} from '@shared/config/config_schema.js'; -import fs from 'fs'; -import path from 'path'; - -function createWebsocketServer(): Promise<{wss: WebSocketServer; address: string}> { - return new Promise(resolve => { - const server = http.createServer(); - const wss = new WebSocketServer({server}); - - server.listen(0, '127.0.0.1', () => { - const {port} = server.address() as AddressInfo; - resolve({wss, address: `ws://127.0.0.1:${port}`}); - }); - }); -} - -describe('Transcription engine', it => { - const apiKey = 'SOME_KEY'; - async function createTranscriptionEngine() { - const {wss, address} = await createWebsocketServer(); - - const websocketConnected = new Promise(resolve => { - wss.once('connection', socket => setImmediate(() => resolve(socket))); - }); - - const te = new TranscriptionEngine({whisper: {endpoint: address, apiKey}} as ConfigType, fakeLogger()); - - return {wss, websocketConnected, te}; - } - - function cleanup(te: TranscriptionEngine, wss: WebSocketServer) { - te.disconnectWhisperService(); - wss.close(); - } - - it('connects to whisper service', async () => { - const {wss, websocketConnected, te} = await createTranscriptionEngine(); - - te.connectWhisperService(); - await expect(websocketConnected).resolves.toBeTruthy(); - - cleanup(te, wss); - }); - - it('disconnects existing connection if connect is called again', async () => { - const {wss, websocketConnected, te} = await createTranscriptionEngine(); - - te.connectWhisperService(); - const socket = await websocketConnected; - const socketClose = new Promise(resolve => socket.on('close', () => resolve(true))); - - // Wait for transcription engine's socket to be in ready state - await new Promise(r => setTimeout(r, 1000)); - - te.connectWhisperService(); - await expect(socketClose).resolves.toBeTruthy(); - - cleanup(te, wss); - }); - - it('sends apiKey after connecting to whisper service', async () => { - const {wss, websocketConnected, te} = await createTranscriptionEngine(); - - te.connectWhisperService(); - const socket = await websocketConnected; - - const receivedMessage = new Promise(resolve => { - socket.once('message', data => resolve(JSON.parse(data.toString()))); - }); - - await expect(receivedMessage).resolves.toEqual({api_key: apiKey}); - - cleanup(te, wss); - }); - - it('forwards audio chunks to whisper service', async () => { - const {wss, websocketConnected, te} = await createTranscriptionEngine(); - te.connectWhisperService(); - const socket = await websocketConnected; - - // Wait for transcription engine's socket to be in ready state - await new Promise(r => setTimeout(r, 1000)); - - const receivedChunks: Array = []; - socket.on('message', data => receivedChunks.push(data as Buffer)); - - const audioFileDir = path.join(__dirname, '../../../../test-audio-files/wikipedia-.fun/chunked'); - const chunkFiles = fs.readdirSync(audioFileDir); - const chunks = []; - for (const file of chunkFiles) { - const chunk = fs.readFileSync(path.join(audioFileDir, file)); - chunks.push(chunk); - te.forwardMessage(chunk, true); - } - - await new Promise(r => setTimeout(r, 1000)); - - expect(receivedChunks.length).toBe(chunks.length); - for (let i = 0; i < chunks.length; i++) { - expect(chunks[i].compare(receivedChunks[i])).toBe(0); - } - - cleanup(te, wss); - }); - - it('forwards model selection message to whisper service', async () => { - const {wss, websocketConnected, te} = await createTranscriptionEngine(); - te.connectWhisperService(); - const socket = await websocketConnected; - - // Wait for transcription engine's socket to be in ready state - await new Promise(r => setTimeout(r, 1000)); - - const receivedMessage = new Promise(resolve => { - socket.once('message', data => resolve(JSON.parse(data.toString()))); - }); - - const message = {model_key: 'selected_key', feature_selection: {}}; - te.forwardMessage(Buffer.from(JSON.stringify(message)), false); - - await new Promise(r => setTimeout(r, 1000)); - - await expect(receivedMessage).resolves.toEqual(message); - - cleanup(te, wss); - }); - - it('emits transcription events when transcriptions are received', async () => { - const {wss, websocketConnected, te} = await createTranscriptionEngine(); - te.connectWhisperService(); - const socket = await websocketConnected; - - const receivedBlocks: Array = []; - te.on('transcription', block => receivedBlocks.push(block)); - - const transcriptions: Array = [ - {type: BackendTranscriptBlockType.InProgress, text: 'Hello', start: 0, end: 1}, - {type: BackendTranscriptBlockType.InProgress, text: 'this is some', start: 1, end: 2}, - {type: BackendTranscriptBlockType.Final, text: 'Hello, this is some', start: 0, end: 2}, - {type: BackendTranscriptBlockType.InProgress, text: 'test transcription', start: 2, end: 3}, - {type: BackendTranscriptBlockType.Final, text: 'test transcriptions.', start: 2, end: 3}, - ]; - for (const block of transcriptions) { - socket.send(JSON.stringify(block)); - } - - await new Promise(r => setTimeout(r, 1000)); - - expect(receivedBlocks).toEqual(transcriptions); - - cleanup(te, wss); - }); - - it('emits sourceMessage but not transcription event when non transcription JSON message is received', async () => { - const {wss, websocketConnected, te} = await createTranscriptionEngine(); - te.connectWhisperService(); - const socket = await websocketConnected; - - const receivedBlocks: Array = []; - const receivedMessages: Array = []; - te.on('transcription', block => receivedBlocks.push(block)); - te.on('sourceMessage', message => receivedMessages.push(message)); - - const sentMessages: Array = [ - {some: 'other', message: 'object'}, - {other: 'message', from: {whisper: ['service']}}, - ]; - for (const block of sentMessages) { - socket.send(JSON.stringify(block)); - } - - await new Promise(r => setTimeout(r, 1000)); - - expect(receivedBlocks).toEqual([]); - expect(receivedMessages).toEqual(sentMessages); - - cleanup(te, wss); - }); -}); diff --git a/node-server/src/server/services/transcription_engine.ts b/node-server/src/server/services/transcription_engine.ts deleted file mode 100644 index 1580dec..0000000 --- a/node-server/src/server/services/transcription_engine.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type {ConfigType} from '@shared/config/config_schema.js'; -import type {Logger} from '@shared/logger/logger.js'; -import {Type, type Static} from '@sinclair/typebox'; -import {Value} from '@sinclair/typebox/value'; -import {TypedEmitter} from 'tiny-typed-emitter'; -import WebSocket from 'ws'; - -export enum BackendTranscriptBlockType { - Final = 0, - InProgress = 1, -} - -const BACKEND_TRANSCRIPT_BLOCK_SCHEMA = Type.Object( - { - type: Type.Enum(BackendTranscriptBlockType), - start: Type.Number(), - end: Type.Number(), - text: Type.String(), - }, - {additionalProperties: false}, -); - -export type BackendTranscriptBlock = Static; - -export type AudioTranscriptEvents = { - transcription: (block: BackendTranscriptBlock) => unknown; - sourceMessage: (message: JSON) => unknown; -}; - -export default class TranscriptionEngine extends TypedEmitter { - private _ws?: WebSocket; - private _log: Logger; - - constructor( - private _config: ConfigType, - log: Logger, - ) { - super(); - this._log = log.child({service: 'TranscriptionEngine'}); - } - - /** - * Initializes a new connection to whisper service - */ - connectWhisperService() { - if (this._ws) { - this._log.debug('Closing existing websocket before initializing new connection'); - - try { - this._ws.close(); - } catch (err) { - this._log.warn({msg: 'Failed to close existing websocket before initializing new connection', err}); - } - } - - const ws = new WebSocket(this._config.whisper.endpoint); - ws.once('open', () => { - this._log.info('Connected to whisper service'); - ws.send( - JSON.stringify({ - api_key: this._config.whisper.apiKey, - }), - ); - - this._ws = ws; - ws.on('message', data => { - let message; - try { - message = JSON.parse(data.toString()); - } catch (err) { - this._log.error({msg: 'Failed to parse message from whisper service', err, message: data.toString()}); - return; - } - const isTranscriptBlock = Value.Check(BACKEND_TRANSCRIPT_BLOCK_SCHEMA, message); - - if (isTranscriptBlock) { - this._log.trace({msg: 'Emiting transcript transcript event', block: message}); - this.emit('transcription', message); - } else { - this._log.trace({msg: 'Emiting source message event', message}); - this.emit('sourceMessage', message); - } - }); - }); - - ws.on('error', err => { - this._log.error({msg: 'Whisper service websocket encountered an error', err}); - }); - - ws.on('close', code => { - this._log.info(`Whisper service connection closed with code ${code}`); - }); - } - - /** - * Disconnects the existing whisper service connection - */ - disconnectWhisperService() { - if (this._ws) { - this._log.debug('Closing existing websocket'); - - try { - this._ws.close(); - } catch (err) { - this._log.warn({msg: 'Failed to close existing websocket', err}); - } - } else { - this._log.trace('No existing websocket to disconnect'); - } - } - - /** - * Forward a message to whisper service - * @param message message to send - * @param isBinary if message is binary or not - */ - forwardMessage(data: WebSocket.RawData, isBinary: boolean) { - if (this._ws) { - const message = isBinary ? data : data.toString(); - try { - this._log.trace({msg: 'Forwarding message to whisper server', message}); - this._ws.send(message); - } catch (err) { - this._log.error({msg: 'Error while forwarding message to whisper server', data, err}); - } - } else { - this._log.debug({msg: "Can't forward message to whisper service because websocket doesn't exist", data}); - } - } -} diff --git a/node-server/src/shared/config/config_schema.ts b/node-server/src/shared/config/config_schema.ts deleted file mode 100644 index dc7f1e6..0000000 --- a/node-server/src/shared/config/config_schema.ts +++ /dev/null @@ -1,82 +0,0 @@ -import {Type} from '@sinclair/typebox'; - -export enum NodeEnv { - Development = 'development', - Production = 'production', - Test = 'test', -} - -export enum LogLevel { - Silent = 'silent', - Trace = 'trace', - Debug = 'debug', - Info = 'info', - Warn = 'warn', - Error = 'error', -} - -// Define environment schema -const RUNTIME_CONFIG = Type.Object({ - NODE_ENV: Type.Enum(NodeEnv, {default: NodeEnv.Production}), - LOG_LEVEL: Type.Enum(LogLevel, {default: LogLevel.Info}), -}); - -const SERVER_CONFIG = Type.Object({ - HOST: Type.String({default: 'localhost'}), - PORT: Type.Number({default: 8080}), - CORS_ORIGIN: Type.String({default: '*'}), - SERVER_ADDRESS: Type.String({default: '127.0.0.1:8080'}), -}); - -const WHISPER_CONFIG = Type.Object({ - WHISPER_SERVICE_ENDPOINT: Type.String({minLength: 1}), - API_KEY: Type.String({minLength: 1}), -}); - -const AUTH_CONFIG = Type.Union([ - Type.Object({ - REQUIRE_AUTH: Type.Const(false), - }), - Type.Object({ - REQUIRE_AUTH: Type.Const(true), - SOURCE_TOKEN: Type.String({minLength: 1}), - ACCESS_TOKEN_BYTES: Type.Number({minimum: 1}), - ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Number({minimum: 1}), - ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Number({minimum: 1}), - SESSION_TOKEN_BYTES: Type.Number({minimum: 1}), - SESSION_LENGTH_SEC: Type.Number({minimum: 1}), - }), -]); - -// Merge all configs into one schema -export const SCHEMA = Type.Intersect([RUNTIME_CONFIG, SERVER_CONFIG, WHISPER_CONFIG, AUTH_CONFIG]); - -export type ConfigType = Readonly<{ - nodeEnv: NodeEnv; - isDevelopment: boolean; - isProduction: boolean; - log: { - level: LogLevel; - }; - server: { - host: string; - port: number; - corsOrigin: string; - serverAddress: string; - }; - whisper: { - endpoint: string; - apiKey: string; - }; - auth: - | {required: false} - | { - required: true; - sourceToken: string; - accessTokenBytes: number; - accessTokenRefreshIntervalSec: number; - accessTokenValidPeriodSec: number; - sessionTokenBytes: number; - sessionLengthSec: number; - }; -}>; diff --git a/node-server/src/shared/config/load_config.ts b/node-server/src/shared/config/load_config.ts deleted file mode 100644 index f8279ab..0000000 --- a/node-server/src/shared/config/load_config.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {Ajv} from 'ajv'; -import {Value} from '@sinclair/typebox/value'; -import type {Static} from '@sinclair/typebox'; -import {NodeEnv, SCHEMA, type ConfigType} from './config_schema.js'; -import {configDotenv} from 'dotenv'; - -/** - * Loads environment configuration file from privated path - * Checks that configuration is correctly formatted - * @param path file path to a dotenv file - * @returns configuration - */ -export default function loadConfig(path?: string): ConfigType { - // Load from .env file - configDotenv({path}); - - const ajv = new Ajv({ - allErrors: true, - useDefaults: true, - coerceTypes: true, - allowUnionTypes: true, - }); - - const defaults = Value.Default(SCHEMA, {}) as Static; - const env = Object.assign(defaults, process.env); - const valid = ajv.validate(SCHEMA, env); - if (!valid) { - const error = new Error('Invalid Configuration! ' + ajv.errorsText()); - (error as unknown as {errors: unknown}).errors = ajv.errors; - throw error; - } - - // Application configuration object - const config: ConfigType = Object.freeze({ - nodeEnv: env.NODE_ENV, - isDevelopment: env.NODE_ENV === NodeEnv.Development, - isProduction: env.NODE_ENV === NodeEnv.Production, - log: { - level: env.LOG_LEVEL, - }, - server: { - host: env.HOST, - port: env.PORT, - corsOrigin: env.CORS_ORIGIN, - serverAddress: env.SERVER_ADDRESS, - }, - whisper: { - endpoint: env.WHISPER_SERVICE_ENDPOINT, - apiKey: env.API_KEY, - }, - auth: { - required: env.REQUIRE_AUTH, - sourceToken: env.SOURCE_TOKEN, - accessTokenBytes: env.ACCESS_TOKEN_BYTES, - accessTokenRefreshIntervalSec: env.ACCESS_TOKEN_REFRESH_INTERVAL_SEC, - accessTokenValidPeriodSec: env.ACCESS_TOKEN_VALID_PERIOD_SEC, - sessionTokenBytes: env.SESSION_TOKEN_BYTES, - sessionLengthSec: env.SESSION_LENGTH_SEC, - }, - }) as unknown as ConfigType; - - return config; -} diff --git a/node-server/src/shared/logger/logger.test.ts b/node-server/src/shared/logger/logger.test.ts deleted file mode 100644 index 06e21e2..0000000 --- a/node-server/src/shared/logger/logger.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {LogLevel, type ConfigType} from '@shared/config/config_schema.js'; -import logger from '@shared/logger/logger.js'; -import {describe, expect, vi} from 'vitest'; -import os from 'os'; -import formatTestNames from '@test/utils/format_test_names.js'; - -describe('Logger', it => { - const time = 12345; - - const logLines = [ - `{"level":10,"time":${time},"pid":${process.pid},"hostname":"${os.hostname()}","msg":"trace"}\n`, - `{"level":20,"time":${time},"pid":${process.pid},"hostname":"${os.hostname()}","msg":"debug"}\n`, - `{"level":30,"time":${time},"pid":${process.pid},"hostname":"${os.hostname()}","msg":"info"}\n`, - `{"level":40,"time":${time},"pid":${process.pid},"hostname":"${os.hostname()}","msg":"warn"}\n`, - `{"level":50,"time":${time},"pid":${process.pid},"hostname":"${os.hostname()}","msg":"error"}\n`, - `{"level":60,"time":${time},"pid":${process.pid},"hostname":"${os.hostname()}","msg":"fatal"}\n`, - ]; - - it.for( - formatTestNames([ - {name: 'silent', level: LogLevel.Silent, expected: []}, - {name: 'trace', level: LogLevel.Trace, expected: logLines.slice(0)}, - {name: 'debug', level: LogLevel.Debug, expected: logLines.slice(1)}, - {name: 'info', level: LogLevel.Info, expected: logLines.slice(2)}, - {name: 'warn', level: LogLevel.Warn, expected: logLines.slice(3)}, - {name: 'error', level: LogLevel.Error, expected: logLines.slice(4)}, - ]), - )('logs at configured log level: %s', ([, {level, expected}]) => { - vi.useFakeTimers(); - vi.setSystemTime(new Date(time)); - - const stdout = vi.fn(); - process.stdout.write = stdout; - - const log = logger({log: {level}} as ConfigType); - - log.trace('trace'); - log.debug('debug'); - log.info('info'); - log.warn('warn'); - log.error('error'); - log.fatal('fatal'); - - const createLog = stdout.mock.calls.reduce((log, line) => log + line[0], ''); - expect(createLog).toBe(expected.reduce((log, line) => log + line, '')); - }); -}); diff --git a/node-server/src/shared/logger/logger.ts b/node-server/src/shared/logger/logger.ts deleted file mode 100644 index 8d167c3..0000000 --- a/node-server/src/shared/logger/logger.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {pino} from 'pino'; -import type {ConfigType} from '../config/config_schema.js'; - -export type Logger = pino.Logger; - -/** - * Creates a logger instance using app configuration - * @param config configuration object - * @returns logger instance - */ -export default function createLogger(config: ConfigType): Logger { - const logger = pino({ - level: config.log.level, - serializers: {err: pino.stdSerializers.errWithCause}, - }); - return logger; -} diff --git a/node-server/template.env b/node-server/template.env deleted file mode 100644 index e187e4d..0000000 --- a/node-server/template.env +++ /dev/null @@ -1,28 +0,0 @@ -NODE_ENV=development -LOG_LEVEL=info - -# ### Host and port API webserver should listen on -HOST=0.0.0.0 -PORT=8080 -CORS_ORIGIN=* -SERVER_ADDRESS=127.0.0.1:8080 - -# ### URL to whisper service -WHISPER_SERVICE_ENDPOINT=ws://127.0.0.1:8000/sourcesink -API_KEY=CHANGEME - -# ### Authentication settings -# Enable or disable authentication -REQUIRE_AUTH=true -# Key used by frontend to connect as audio source -SOURCE_TOKEN=CHANGEME -# How many bytes of random data should be used to generate access token -ACCESS_TOKEN_BYTES=8 -# How often access token should be refreshed in seconds -ACCESS_TOKEN_REFRESH_INTERVAL_SEC=150 -# How long a single access token is valid for after generation in seconds -ACCESS_TOKEN_VALID_PERIOD_SEC=300 -# How many bytes of random data should be used to generate session token -SESSION_TOKEN_BYTES=32 -# How long a single session token is valid for after generation in seconds -SESSION_LENGTH_SEC=5400 \ No newline at end of file diff --git a/node-server/test/fakes/fake_logger.ts b/node-server/test/fakes/fake_logger.ts deleted file mode 100644 index 6823c4d..0000000 --- a/node-server/test/fakes/fake_logger.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type {Logger} from '@shared/logger/logger.js'; -import {vi, type Mocked} from 'vitest'; - -export default function fakeLogger(): Mocked { - const fakePino = { - silent: vi.fn(), - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - fatal: vi.fn(), - trace: vi.fn(), - child: vi.fn().mockImplementation(() => fakePino), - }; - return fakePino as unknown as Mocked; -} diff --git a/node-server/test/utils/format_test_names.ts b/node-server/test/utils/format_test_names.ts deleted file mode 100644 index df58807..0000000 --- a/node-server/test/utils/format_test_names.ts +++ /dev/null @@ -1,46 +0,0 @@ -interface UnnamedTestItem { - [key: string]: unknown; - name?: never; - params?: never; -} - -interface NamedTestItem { - [key: string]: unknown; - name: string; -} - -interface UnnamedNestedTestItem { - [key: string]: unknown; - name?: never; - params: unknown; -} - -type TestItem = UnnamedTestItem | NamedTestItem | UnnamedNestedTestItem; - -type FormattedTestItem = T extends NamedTestItem ? [T['name'], Omit] : [string, T]; - -/** - * Takes in an array of objects and returns an array of tuples - * First element of tuple is the generated name of a test - * Second element of tuple is the provided test parameters - * If "name" parameter is provided, generated name is "name" and "name" is removed from parameters - * If "params" parameter is provided and "name" is not provided, generated name is "params" and "params" is kept in parameters - * Otherwise, name is JSON stringified version of given object and parameters are left unchanged - * @param tests - * @returns Formatted tuples - */ -export default function formatTestNames(tests: Array): Array> { - return tests.map(t => { - if (typeof t.name === 'string') { - const {name, ...params} = t; - return [name, params] as FormattedTestItem; - } - - if ('params' in t) { - const {params, ...rest} = t; - return [JSON.stringify(params), {params, ...rest}] as FormattedTestItem; - } - - return [JSON.stringify(t), t] as FormattedTestItem; - }); -} diff --git a/node-server/tsconfig.json b/node-server/tsconfig.json deleted file mode 100644 index 8f4f0ea..0000000 --- a/node-server/tsconfig.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "extends": "./node_modules/gts/tsconfig-google.json", - "compilerOptions": { - "rootDir": ".", - "module": "NodeNext", - "outDir": "build", - "esModuleInterop": true, - "skipLibCheck": true, - "plugins": [ - { - "transform": "typescript-transform-paths" - } - ], - "paths": { - "@shared/*": [ - "./src/shared/*" - ], - "@server/*": [ - "./src/server/*" - ], - "@src/*": [ - "./src/*" - ], - "@test/*": [ - "./test/*" - ] - } - }, - "include": [ - "src/**/*.ts", - "test/**/*.ts", - ], - "exclude": [ - "build" - ] -} \ No newline at end of file diff --git a/node-server/vite.config.ts b/node-server/vite.config.ts deleted file mode 100644 index 7bfd70d..0000000 --- a/node-server/vite.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import path from 'path' -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['./src/**/*.test.ts'], - coverage: { - enabled: true, - provider: 'istanbul', // or 'v8' - include: ['src'], - exclude: ['src/index.ts'] - }, - }, - resolve: { - alias: { - '@shared': path.resolve(__dirname, './src/shared'), - '@server': path.resolve(__dirname, './src/server'), - '@src': path.resolve(__dirname, './src/'), - '@test': path.resolve(__dirname, './test/'), - }, - }, -}) diff --git a/package-lock.json b/package-lock.json index 4e77fcc..04355b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,5090 @@ { - "name": "ScribeAR-NodeServer", + "name": "scribear", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "name": "scribear", + "workspaces": [ + "apps/*", + "libs/*" + ], + "devDependencies": { + "@eslint-react/eslint-plugin": "2.2.2", + "@eslint/js": "9.38.0", + "@trivago/prettier-plugin-sort-imports": "5.2.2", + "@types/node": "24.9.1", + "@vitest/coverage-istanbul": "3.2.4", + "@vitest/ui": "3.2.4", + "eslint": "9.38.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-react-hooks": "7.0.0", + "eslint-plugin-react-refresh": "0.4.24", + "globals": "16.4.0", + "nodemon": "3.1.10", + "pino-pretty": "13.1.2", + "prettier": "3.6.2", + "typescript": "5.9.3", + "typescript-eslint": "8.46.2", + "vitest": "3.2.4", + "vitest-mock-extended": "3.1.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "apps/session-manager": { + "name": "@scribear/session-manager", + "version": "0.0.0", + "dependencies": { + "@fastify/awilix": "8.0.0", + "@fastify/swagger": "9.5.2", + "awilix": "12.0.5", + "env-schema": "6.1.0", + "typebox": "1.0.41" + } + }, + "libs/base-fastify-server": { + "name": "@scribear/base-fastify-server", + "version": "0.0.0", + "dependencies": { + "@fastify/awilix": "8.0.0", + "@fastify/cookie": "11.0.2", + "@fastify/helmet": "13.0.2", + "@fastify/sensible": "6.0.3", + "@fastify/swagger": "9.5.2", + "@fastify/swagger-ui": "5.2.3", + "@fastify/type-provider-typebox": "6.1.0", + "@fastify/websocket": "11.2.0", + "awilix": "12.0.5", + "fastify": "5.6.1", + "fastify-plugin": "5.1.0", + "pino": "10.1.0", + "typebox": "1.0.41", + "uuid": "13.0.0", + "ws": "8.18.3" + } + }, + "libs/base-schema": { + "name": "@scribear/base-schema", + "version": "0.0.0", + "dependencies": { + "typebox": "1.0.41" + } + }, + "libs/session-manager-schema": { + "name": "@scribear/session-manager-schema", + "version": "0.0.0", + "dependencies": { + "typebox": "1.0.41" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "peer": true, + "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/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint-react/ast": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/eff": "2.2.2", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/typescript-estree": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "string-ts": "^2.2.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/core": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.2", + "@eslint-react/eff": "2.2.2", + "@eslint-react/shared": "2.2.2", + "@eslint-react/var": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "birecord": "^0.1.1", + "ts-pattern": "^5.8.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/eff": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/eslint-plugin": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/eff": "2.2.2", + "@eslint-react/shared": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/type-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "eslint-plugin-react-dom": "2.2.2", + "eslint-plugin-react-hooks-extra": "2.2.2", + "eslint-plugin-react-naming-convention": "2.2.2", + "eslint-plugin-react-web-api": "2.2.2", + "eslint-plugin-react-x": "2.2.2", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^9.37.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@eslint-react/shared": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/eff": "2.2.2", + "@typescript-eslint/utils": "^8.46.1", + "ts-pattern": "^5.8.0", + "zod": "^4.1.12" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint-react/var": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.2", + "@eslint-react/eff": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "ts-pattern": "^5.8.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.17.1", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/@fastify/awilix": { + "version": "8.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "awilix-manager": "^6.0.0", + "fastify-plugin": "^5.0.1" + }, + "peerDependencies": { + "awilix": ">=9.0.0", + "fastify": "^5.0.0" + } + }, + "node_modules/@fastify/cookie": { + "version": "11.0.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "cookie": "^1.0.0", + "fastify-plugin": "^5.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/helmet": { + "version": "13.0.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "helmet": "^8.0.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/sensible": { + "version": "6.0.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "dequal": "^2.0.3", + "fastify-plugin": "^5.0.0", + "forwarded": "^0.2.0", + "http-errors": "^2.0.0", + "type-is": "^1.6.18", + "vary": "^1.1.2" + } + }, + "node_modules/@fastify/static": { + "version": "8.3.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@fastify/static/node_modules/glob": { + "version": "11.0.3", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/jackspeak": { + "version": "4.1.1", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/lru-cache": { + "version": "11.2.2", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@fastify/static/node_modules/minimatch": { + "version": "10.0.3", + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/path-scurry": { + "version": "2.0.0", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/swagger": { + "version": "9.5.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^3.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, + "node_modules/@fastify/swagger-ui": { + "version": "5.2.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/static": "^8.0.0", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + } + }, + "node_modules/@fastify/type-provider-typebox": { + "version": "6.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peerDependencies": { + "typebox": "^1.0.13" + } + }, + "node_modules/@fastify/websocket": { + "version": "11.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.3", + "fastify-plugin": "^5.0.0", + "ws": "^8.16.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@scribear/base-fastify-server": { + "resolved": "libs/base-fastify-server", + "link": true + }, + "node_modules/@scribear/base-schema": { + "resolved": "libs/base-schema", + "link": true + }, + "node_modules/@scribear/session-manager": { + "resolved": "apps/session-manager", + "link": true + }, + "node_modules/@scribear/session-manager-schema": { + "resolved": "libs/session-manager-schema", + "link": true + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/coverage-istanbul": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@istanbuljs/schema": "^0.1.3", + "debug": "^4.4.1", + "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", + "magicast": "^0.3.5", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.1.0", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/awilix": { + "version": "12.0.5", + "license": "MIT", + "peer": true, + "dependencies": { + "camel-case": "^4.1.2", + "fast-glob": "^3.3.3" + }, + "engines": { + "node": ">=16.3.0" + } + }, + "node_modules/awilix-manager": { + "version": "6.1.0", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "awilix": ">=9.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.19", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/birecord": { + "version": "0.1.1", + "dev": true, + "license": "(MIT OR Apache-2.0)" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "dev": true, + "license": "MIT" + }, + "node_modules/compare-versions": { + "version": "6.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.238", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-schema": { + "version": "6.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "dotenv": "^17.0.0", + "dotenv-expand": "10.0.0" + } + }, + "node_modules/env-schema/node_modules/ajv": { + "version": "8.17.1", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/env-schema/node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.11", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-dom": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.2", + "@eslint-react/core": "2.2.2", + "@eslint-react/eff": "2.2.2", + "@eslint-react/shared": "2.2.2", + "@eslint-react/var": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "compare-versions": "^6.1.1", + "string-ts": "^2.2.1", + "ts-pattern": "^5.8.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^9.37.0", + "typescript": "^5.9.3" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.22.4 || ^4.0.0", + "zod-validation-error": "^3.0.3 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks-extra": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.2", + "@eslint-react/core": "2.2.2", + "@eslint-react/eff": "2.2.2", + "@eslint-react/shared": "2.2.2", + "@eslint-react/var": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/type-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "string-ts": "^2.2.1", + "ts-pattern": "^5.8.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "eslint": "^9.37.0", + "typescript": "^5.9.3" + } + }, + "node_modules/eslint-plugin-react-naming-convention": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.2", + "@eslint-react/core": "2.2.2", + "@eslint-react/eff": "2.2.2", + "@eslint-react/shared": "2.2.2", + "@eslint-react/var": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/type-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "string-ts": "^2.2.1", + "ts-pattern": "^5.8.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^9.37.0", + "typescript": "^5.9.3" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react-web-api": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.2", + "@eslint-react/core": "2.2.2", + "@eslint-react/eff": "2.2.2", + "@eslint-react/shared": "2.2.2", + "@eslint-react/var": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "string-ts": "^2.2.1", + "ts-pattern": "^5.8.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^9.37.0", + "typescript": "^5.9.3" + } + }, + "node_modules/eslint-plugin-react-x": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.2.2", + "@eslint-react/core": "2.2.2", + "@eslint-react/eff": "2.2.2", + "@eslint-react/shared": "2.2.2", + "@eslint-react/var": "2.2.2", + "@typescript-eslint/scope-manager": "^8.46.1", + "@typescript-eslint/type-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "@typescript-eslint/utils": "^8.46.1", + "compare-versions": "^6.1.1", + "is-immutable-type": "^5.0.1", + "string-ts": "^2.2.1", + "ts-api-utils": "^2.1.0", + "ts-pattern": "^5.8.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^9.37.0", + "typescript": "^5.9.3" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.1.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.17.1", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.6.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastify/node_modules/pino": { + "version": "9.14.0", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "9.3.0", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-immutable-type": { + "version": "5.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@typescript-eslint/type-utils": "^8.0.0", + "ts-api-utils": "^2.0.0", + "ts-declaration-location": "^1.0.4" + }, + "peerDependencies": { + "eslint": "*", + "typescript": ">=4.7.4" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "dev": true, + "license": "MIT" + }, + "node_modules/joycon": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-resolver": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fast-uri": "^3.0.5", + "rfdc": "^1.1.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.26", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.1.0", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.6", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.52.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@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.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-ts": { + "version": "2.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-declaration-location": { + "version": "1.0.7", + "dev": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": ">=4.0.0" + } + }, + "node_modules/ts-declaration-location/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/ts-essentials": { + "version": "10.1.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ts-pattern": { + "version": "5.8.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typebox": { + "version": "1.0.41", + "license": "MIT", + "peer": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.0", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.1.11", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest-mock-extended": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-essentials": ">=10.0.0" + }, + "peerDependencies": { + "typescript": "3.x || 4.x || 5.x", + "vitest": ">=3.0.0" + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } } diff --git a/package.json b/package.json new file mode 100644 index 0000000..98b3a05 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "scribear", + "description": "", + "author": "scribear", + "private": true, + "type": "module", + "workspaces": [ + "apps/*", + "libs/*" + ], + "scripts": { + "build": "npm run build --workspaces --if-present", + "format": "npm run format --workspaces --if-present", + "format:fix": "npm run format:fix --workspaces --if-present", + "lint": "npm run lint --workspaces --if-present", + "lint:fix": "npm run lint:fix --workspaces --if-present", + "test": "npm run test --workspaces --if-present" + }, + "engines": { + "node": ">=24.0.0" + }, + "devDependencies": { + "@eslint-react/eslint-plugin": "2.2.2", + "@eslint/js": "9.38.0", + "@trivago/prettier-plugin-sort-imports": "5.2.2", + "@types/node": "24.9.1", + "@vitest/coverage-istanbul": "3.2.4", + "@vitest/ui": "3.2.4", + "eslint": "9.38.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-react-hooks": "7.0.0", + "eslint-plugin-react-refresh": "0.4.24", + "globals": "16.4.0", + "nodemon": "3.1.10", + "pino-pretty": "13.1.2", + "prettier": "3.6.2", + "typescript": "5.9.3", + "typescript-eslint": "8.46.2", + "vitest": "3.2.4", + "vitest-mock-extended": "3.1.0" + } +} diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..35c1521 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,34 @@ +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + * + * This config object configures Prettier + * Prettier is a code formatter that helps mantain a consistent code style through our project + * This helps: + * - Make code easier to read + * - Avoid commiting "invisible" code changes caused by differences in developer environments (editor, OS, preferences) + */ +const config = { + tabWidth: 2, + useTabs: false, + endOfLine: 'lf', + printWidth: 80, + + semi: true, + singleQuote: true, + jsxSingleQuote: false, + + // Enforce import grouping and ordering + importOrder: [ + '^react(.*)', + '^@mui/(.*)', + '', + '@scribear/(.*)', + '^[(./|../)]', + ], + importOrderSeparation: true, + importOrderSortSpecifiers: true, + plugins: ['@trivago/prettier-plugin-sort-imports'], +}; + +export default config; diff --git a/test-audio-files/wikipedia-.fun/En-.fun-article.ogg b/test-audio-files/wikipedia-.fun/En-.fun-article.ogg deleted file mode 100644 index 991be31..0000000 Binary files a/test-audio-files/wikipedia-.fun/En-.fun-article.ogg and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/about.md b/test-audio-files/wikipedia-.fun/about.md deleted file mode 100644 index 2140d50..0000000 --- a/test-audio-files/wikipedia-.fun/about.md +++ /dev/null @@ -1,7 +0,0 @@ -`En-.fun-article.ogg` audio file was obtained from https://en.wikipedia.org/wiki/File:En-.fun-article.ogg on Feb 24, 2025. -Speaker: Clay2004, CC BY-SA 4.0, via Wikimedia Commons - -It was then converted into .wav and split into 1 second chunks using ffmpeg using the following command: -``` -ffmpeg -i En-.fun-article.ogg -ac 1 -ar 16000 -f segment -segment_time 1 ./chunked/chunk_%03d.wav -``` diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_000.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_000.wav deleted file mode 100644 index ebe8886..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_000.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_001.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_001.wav deleted file mode 100644 index 99f6eb6..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_001.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_002.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_002.wav deleted file mode 100644 index 160c632..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_002.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_003.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_003.wav deleted file mode 100644 index b93c43f..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_003.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_004.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_004.wav deleted file mode 100644 index a5bfccc..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_004.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_005.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_005.wav deleted file mode 100644 index b3185c8..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_005.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_006.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_006.wav deleted file mode 100644 index c3f3e85..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_006.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_007.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_007.wav deleted file mode 100644 index 0d8cb96..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_007.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_008.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_008.wav deleted file mode 100644 index ae3e248..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_008.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_009.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_009.wav deleted file mode 100644 index e183e42..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_009.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_010.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_010.wav deleted file mode 100644 index d3548e7..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_010.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_011.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_011.wav deleted file mode 100644 index 6b8214f..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_011.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_012.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_012.wav deleted file mode 100644 index bed3fad..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_012.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_013.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_013.wav deleted file mode 100644 index 4abcea7..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_013.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_014.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_014.wav deleted file mode 100644 index 0581408..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_014.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_015.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_015.wav deleted file mode 100644 index b9e4890..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_015.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_016.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_016.wav deleted file mode 100644 index 2e12897..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_016.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_017.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_017.wav deleted file mode 100644 index 13af25a..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_017.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_018.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_018.wav deleted file mode 100644 index ba83abe..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_018.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_019.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_019.wav deleted file mode 100644 index bc50fc5..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_019.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_020.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_020.wav deleted file mode 100644 index dc525d5..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_020.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_021.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_021.wav deleted file mode 100644 index fe25c02..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_021.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_022.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_022.wav deleted file mode 100644 index a66fa12..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_022.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_023.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_023.wav deleted file mode 100644 index db18ed1..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_023.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_024.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_024.wav deleted file mode 100644 index 9eda3df..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_024.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_025.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_025.wav deleted file mode 100644 index 7885cd1..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_025.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_026.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_026.wav deleted file mode 100644 index 19653c9..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_026.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_027.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_027.wav deleted file mode 100644 index d0f28fc..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_027.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_028.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_028.wav deleted file mode 100644 index 04b91f0..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_028.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_029.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_029.wav deleted file mode 100644 index d599137..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_029.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_030.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_030.wav deleted file mode 100644 index e8988f8..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_030.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_031.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_031.wav deleted file mode 100644 index 4daa089..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_031.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_032.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_032.wav deleted file mode 100644 index 3b5e72c..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_032.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_033.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_033.wav deleted file mode 100644 index ca816cc..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_033.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_034.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_034.wav deleted file mode 100644 index a52b786..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_034.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_035.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_035.wav deleted file mode 100644 index db7ff91..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_035.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_036.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_036.wav deleted file mode 100644 index bb2982f..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_036.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_037.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_037.wav deleted file mode 100644 index 7163214..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_037.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_038.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_038.wav deleted file mode 100644 index 5231ca0..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_038.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_039.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_039.wav deleted file mode 100644 index bbebd92..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_039.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_040.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_040.wav deleted file mode 100644 index 7bbb22b..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_040.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_041.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_041.wav deleted file mode 100644 index 5bd710f..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_041.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_042.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_042.wav deleted file mode 100644 index b746a88..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_042.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_043.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_043.wav deleted file mode 100644 index 9130477..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_043.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_044.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_044.wav deleted file mode 100644 index 006b0db..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_044.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_045.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_045.wav deleted file mode 100644 index 5999342..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_045.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_046.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_046.wav deleted file mode 100644 index 07ef4ae..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_046.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_047.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_047.wav deleted file mode 100644 index 39e342d..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_047.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_048.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_048.wav deleted file mode 100644 index c84a4ed..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_048.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_049.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_049.wav deleted file mode 100644 index e0224a2..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_049.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_050.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_050.wav deleted file mode 100644 index fe48898..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_050.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_051.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_051.wav deleted file mode 100644 index 12f9c36..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_051.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_052.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_052.wav deleted file mode 100644 index 3c81e0c..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_052.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_053.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_053.wav deleted file mode 100644 index 5977356..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_053.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_054.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_054.wav deleted file mode 100644 index 8625599..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_054.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_055.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_055.wav deleted file mode 100644 index 9e0af24..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_055.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_056.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_056.wav deleted file mode 100644 index 049f44d..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_056.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_057.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_057.wav deleted file mode 100644 index 2565b30..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_057.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_058.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_058.wav deleted file mode 100644 index 77514cb..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_058.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_059.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_059.wav deleted file mode 100644 index 5c54790..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_059.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_060.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_060.wav deleted file mode 100644 index bb6810a..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_060.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_061.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_061.wav deleted file mode 100644 index 77d70cf..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_061.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_062.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_062.wav deleted file mode 100644 index b9851ed..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_062.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_063.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_063.wav deleted file mode 100644 index 62dfb12..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_063.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_064.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_064.wav deleted file mode 100644 index b3371ae..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_064.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_065.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_065.wav deleted file mode 100644 index 94bc50b..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_065.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_066.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_066.wav deleted file mode 100644 index 663d96a..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_066.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_067.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_067.wav deleted file mode 100644 index 281aeda..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_067.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_068.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_068.wav deleted file mode 100644 index b23e745..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_068.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_069.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_069.wav deleted file mode 100644 index 575ab78..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_069.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_070.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_070.wav deleted file mode 100644 index fc12462..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_070.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_071.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_071.wav deleted file mode 100644 index 8325165..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_071.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_072.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_072.wav deleted file mode 100644 index a3738f6..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_072.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_073.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_073.wav deleted file mode 100644 index 683a6ec..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_073.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_074.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_074.wav deleted file mode 100644 index b28d94b..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_074.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_075.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_075.wav deleted file mode 100644 index fd30f72..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_075.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_076.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_076.wav deleted file mode 100644 index 13b092c..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_076.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_077.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_077.wav deleted file mode 100644 index 47aefb1..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_077.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_078.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_078.wav deleted file mode 100644 index 2757446..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_078.wav and /dev/null differ diff --git a/test-audio-files/wikipedia-.fun/chunked/chunk_079.wav b/test-audio-files/wikipedia-.fun/chunked/chunk_079.wav deleted file mode 100644 index 80d2615..0000000 Binary files a/test-audio-files/wikipedia-.fun/chunked/chunk_079.wav and /dev/null differ diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..2ad61a4 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,43 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + // Base tsconfig file packages extend from + "compilerOptions": { + // Import settings + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "composite": true, + // Module resolution for monorepo + "baseUrl": ".", + "moduleResolution": "nodenext", + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "es2024", + "lib": [ + "ES2024" + ], + "types": [ + "node" + ], + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + // Style Options + "noImplicitReturns": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..ad24403 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +/** + * @see https://vitest.dev/guide/projects.html#defining-projects + * @see https://vitest.dev/config/ + * Defines the root Vitest configuration. + * It is primarily used to discover and run test files across multiple sub-projects (apps and libs) within the repository. + * It doesn't contain test-specific setup but rather high-level project management. + */ +export default defineConfig({ + test: { + // Configure Vitest to treat each folder containing a vitest.config.ts file a separate project + projects: ['./{apps,libs}/*/vitest.config.ts'], + }, +}); diff --git a/vitest.shared.ts b/vitest.shared.ts new file mode 100644 index 0000000..3925d1c --- /dev/null +++ b/vitest.shared.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config'; + +/** + * @see https://vitest.dev/config/ + * Defines a shared vitest configuration for all packages + */ +export default defineConfig({ + test: { + include: ['./tests/**/*.test.ts'], + coverage: { + enabled: true, + include: ['src'], + provider: 'istanbul', // or 'v8' + // Text to enable quick summary in terminal + // HTML for interactive view, with line by line breakdown + // cobertura for CI/CD actions + reporter: ['text', 'html', 'cobertura'], + }, + }, +}); diff --git a/whisper-service/.dockerignore b/whisper-service/.dockerignore deleted file mode 100644 index ae493f0..0000000 --- a/whisper-service/.dockerignore +++ /dev/null @@ -1,176 +0,0 @@ -device_config.json - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc \ No newline at end of file diff --git a/whisper-service/.gitignore b/whisper-service/.gitignore deleted file mode 100644 index ae493f0..0000000 --- a/whisper-service/.gitignore +++ /dev/null @@ -1,176 +0,0 @@ -device_config.json - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc \ No newline at end of file diff --git a/whisper-service/Dockerfile_CPU b/whisper-service/Dockerfile_CPU deleted file mode 100644 index 054250f..0000000 --- a/whisper-service/Dockerfile_CPU +++ /dev/null @@ -1,16 +0,0 @@ -FROM python:3.12 - -WORKDIR /app - -COPY requirements.txt . - -RUN pip install -r requirements.txt - -COPY . . - -ENV HOST=0.0.0.0 -ENV PORT=80 - -HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --start-interval=1s --retries=3 CMD curl -f http://localhost:80/healthcheck || exit 1 - -CMD ["python", "index.py"] \ No newline at end of file diff --git a/whisper-service/Dockerfile_CUDA b/whisper-service/Dockerfile_CUDA deleted file mode 100644 index a88d1d4..0000000 --- a/whisper-service/Dockerfile_CUDA +++ /dev/null @@ -1,26 +0,0 @@ -FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -y -RUN apt install software-properties-common -y -RUN add-apt-repository ppa:deadsnakes/ppa -RUN apt-get update -y -RUN apt install python3.12 python3-pip python3.12-venv -y -RUN apt install curl -y - -WORKDIR /app - -COPY requirements.txt . - -RUN python3.12 -m venv .venv - -RUN /app/.venv/bin/python -m pip install -r requirements.txt - -COPY . . - -ENV HOST=0.0.0.0 -ENV PORT=80 - -HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --start-interval=1s --retries=3 CMD curl -f http://localhost:80/healthcheck || exit 1 - -CMD ["/app/.venv/bin/python", "index.py"] diff --git a/whisper-service/app_config/init_device_config.py b/whisper-service/app_config/init_device_config.py deleted file mode 100644 index 9062fa0..0000000 --- a/whisper-service/app_config/init_device_config.py +++ /dev/null @@ -1,110 +0,0 @@ -''' -Function to load then initialize whisper service according to device config - -Functions: - init_device_config - -Types: - AvailableFeaturesConfig - ModelConfig - DeviceConfig -''' -import json -import logging -from typing import Any -from model_implementations.import_model_implementation import \ - ModelImplementationId, import_model_implementation -from utils.config_dict_contains import \ - config_dict_contains_dict, config_dict_contains_one_of, config_dict_contains_str -from custom_types.config_types import ModelConfig, DeviceConfig -from custom_types.model_selection_types import SelectionOptions - - -def init_model(device_config: dict[str, Any], key: str) -> ModelConfig: - ''' - Validates and initalizes given model_key in device_config. - Checks if all required property for ModelConfig are present. Throws error if not. - Implementation configuration is checked automatically when implementation is initialized. - Models are initialized by calling load_model() then unload_mode(). - - Parameters: - device_config (dict): Loaded device_config dict - key (str) : model_key to initialize - - Return: - Validated ModelConfig object - ''' - logger = logging.getLogger('uvicorn.error') - - # Grab config specific to model - config_dict_contains_dict(device_config, key) - model_config = device_config[key] - - # Check required properties - config_dict_contains_str(model_config, 'display_name', min_length=1) - config_dict_contains_str(model_config, 'description', min_length=1) - config_dict_contains_one_of( - model_config, 'implementation_id', list(ModelImplementationId)) - config_dict_contains_dict(model_config, 'implementation_configuration') - config_dict_contains_dict(model_config, 'available_features') - - # Initialize the configured model - implementation_id: ModelImplementationId = model_config['implementation_id'] - implementation_config = model_config['implementation_configuration'] - logger.info( - 'Initializing implementation: %s for model_key: %s', implementation_id, key - ) - - implementation = import_model_implementation(implementation_id) - model = implementation({}, implementation_config) - - model.load_model() - model.unload_model() - logger.info( - 'Successfully initialized implementation: %s for model_key: %s', implementation_id, key - ) - - return { - 'display_name': model_config['display_name'], - 'description': model_config['description'], - 'implementation_id': implementation_id, - 'implementation_configuration': implementation_config, - 'available_features': model_config['available_features'] - } - - -def init_device_config(device_config_path: str) -> tuple[DeviceConfig, SelectionOptions]: - ''' - Loads device config file from provided path then initializes configured models. - - - Parameters: - device_config_path (str): Path to device config file - - Returns: - DeviceConfig object and SelectionOptions object - ''' - logger = logging.getLogger('uvicorn.error') - - logger.info('Loading device config from: %s', device_config_path) - with open(device_config_path, 'r', encoding='utf-8') as file: - loaded_config = json.load(file) - - if not isinstance(loaded_config, dict): - raise ValueError('Device config must an object') - - device_config: DeviceConfig = {} - selection_options: SelectionOptions = [] - for key in loaded_config.keys(): - model_config = init_model(loaded_config, key) - - device_config[key] = model_config - - selection_options.append({ - 'model_key': key, - 'display_name': model_config['display_name'], - 'description': model_config['description'], - 'available_features': model_config['available_features'] - }) - - return device_config, selection_options diff --git a/whisper-service/app_config/load_config.py b/whisper-service/app_config/load_config.py deleted file mode 100644 index 6bd6fd6..0000000 --- a/whisper-service/app_config/load_config.py +++ /dev/null @@ -1,35 +0,0 @@ -''' -Helper function to load application configuration - -Functions: - load_config - -Classes: - AppConfig -''' -import os -from dotenv import load_dotenv -from custom_types.config_types import AppConfig - - -def load_config() -> AppConfig: - ''' - Loads application config from .env file. - - Returns: - AppConfig object - ''' - load_dotenv() - - config = AppConfig() - config['API_KEY'] = os.environ.get('API_KEY') - assert len(config['API_KEY']) > 0, 'API_KEY must be non-empty string' - - config['LOG_LEVEL'] = os.environ.get('LOG_LEVEL', 'info') - assert config['LOG_LEVEL'] in ['trace', 'debug', 'info'], \ - 'LOG_LEVEL must be one of: trace, debug, info' - - config['PORT'] = int(os.environ.get('PORT', 8000)) - config['HOST'] = os.environ.get('HOST', '127.0.0.1') - - return config diff --git a/whisper-service/custom_types/authentication_types.py b/whisper-service/custom_types/authentication_types.py deleted file mode 100644 index 8321e91..0000000 --- a/whisper-service/custom_types/authentication_types.py +++ /dev/null @@ -1,14 +0,0 @@ -''' -Type definitions for authentication messages - -Types: - WhisperAuthMessage -''' -from typing import TypedDict - - -class WhisperAuthMessage(TypedDict): - ''' - Type hint for message send by frontnend to authenticate websocket - ''' - api_key: str diff --git a/whisper-service/custom_types/config_types.py b/whisper-service/custom_types/config_types.py deleted file mode 100644 index b526100..0000000 --- a/whisper-service/custom_types/config_types.py +++ /dev/null @@ -1,66 +0,0 @@ -''' -Type definitions for objects used to configure whisper service - -Enums: - ModelImplementationId - -Types: - JsonType - ImplementationModelConfig - AppConfig - AvailableFeaturesConfig - ModelConfig - DeviceConfig -''' -from enum import StrEnum -from typing import TypedDict, Union, List, Dict - - -class AppConfig(TypedDict): - ''' - Object to hold application config loaded from .env file - ''' - API_KEY: str - LOG_LEVEL: str - PORT: int - HOST: str - - -class AvailableFeaturesConfig(TypedDict): - ''' - Type hint for available features configuration dict - Nested within ModelConfig - ''' - - -class ModelImplementationId(StrEnum): - ''' - Unique keys for all available implementations of TranscriptionModelBase - Device config should only select ids from this enum - ''' - MOCK_TRANSCRIPTION_DURATION = "mock_transcription_duration" - FASTER_WHISPER = "faster_whisper" - - -type JsonType = Union[None, int, str, bool, - List[JsonType], Dict[str, JsonType]] - -# Type hint for object used for configuring model implementations -# Used by model implementations and nested in ModelConfig -type ImplementationModelConfig = Dict[str, JsonType] - - -class ModelConfig(TypedDict): - ''' - Type hint for model configuration dict - Nested within DeviceConfig - ''' - display_name: str - description: str - implementation_id: ModelImplementationId - implementation_configuration: ImplementationModelConfig - available_features: AvailableFeaturesConfig - - -# Type hint for loaded device configuration dict -type DeviceConfig = dict[str, ModelConfig] diff --git a/whisper-service/custom_types/model_selection_types.py b/whisper-service/custom_types/model_selection_types.py deleted file mode 100644 index ed8bbe2..0000000 --- a/whisper-service/custom_types/model_selection_types.py +++ /dev/null @@ -1,41 +0,0 @@ -''' -Type definitions for messages used for negotiating model selection - -Types: - ModelOption - SelectionOptions - FeatureSelection - ModelSelection -''' -from typing import TypedDict -from custom_types.config_types import AvailableFeaturesConfig - - -class ModelOption(TypedDict): - ''' - Type hint for a model option available to frontend - Nested within SelectionOptions - ''' - model_key: str - display_name: str - description: str - available_features: AvailableFeaturesConfig - - -# Type hint for available models that is presented to the frontend -type SelectionOptions = list[ModelOption] - - -class FeatureSelection(TypedDict): - ''' - Type hint for user's choice for what features to use - Nested within ModelSelection - ''' - - -class SelectedOption(TypedDict): - ''' - Type hint for message frontend sends to select a model - ''' - model_key: str - feature_selection: FeatureSelection diff --git a/whisper-service/custom_types/transcription_types.py b/whisper-service/custom_types/transcription_types.py deleted file mode 100644 index 76296a1..0000000 --- a/whisper-service/custom_types/transcription_types.py +++ /dev/null @@ -1,28 +0,0 @@ -''' -Type definitions for transcription messages - -Enums: - BackendTranscriptionBlockType - BackendTranscriptBlockType -''' -from enum import IntEnum -from typing import TypedDict - - -class BackendTranscriptionBlockType(IntEnum): - ''' - Possible values for transcription block type value - Enum literal values must match values in node server/frontend - ''' - FINAL = 0 - IN_PROGRESS = 1 - - -class BackendTranscriptBlock(TypedDict): - ''' - Type hint for transcription block messages passed to node server/frontend - ''' - type: BackendTranscriptionBlockType - text: str - start: float - end: float diff --git a/whisper-service/device_config.template.json b/whisper-service/device_config.template.json deleted file mode 100644 index 1031c95..0000000 --- a/whisper-service/device_config.template.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "mock_transcription_duration": { - "display_name": "Sanity Test", - "description": "Returns how many seconds of audio was received by whisper service.", - "implementation_id": "mock_transcription_duration", - "implementation_configuration": {}, - "available_features": {} - }, - "faster-whisper:cpu-tiny-en": { - "display_name": "Tiny Faster Whisper", - "description": "Faster Whisper implementation of Open AI Whisper tiny.en model.", - "implementation_id": "faster_whisper", - "implementation_configuration": { - "model": "tiny.en", - "device": "cpu", - "local_agree_dim": 2, - "min_new_samples": 48000, - "max_segment_samples": 480000, - "silence_threshold": 0.01 - }, - "available_features": {} - } -} diff --git a/whisper-service/index.py b/whisper-service/index.py deleted file mode 100644 index 64be60b..0000000 --- a/whisper-service/index.py +++ /dev/null @@ -1,41 +0,0 @@ -''' -Entry point for whisper-service application. -''' -import sys -import uvicorn -from app_config.load_config import load_config -from app_config.init_device_config import init_device_config -from server.create_server import create_server -from server.helpers.authenticate_websocket import authenticate_websocket -from server.helpers.select_model import select_model -from model_implementations.import_model_implementation import import_model_implementation - - -config = load_config() -device_config, selection_options = init_device_config('device_config.json') - -APP = create_server( - config, - device_config, - selection_options, - import_model_implementation, - authenticate_websocket, - select_model -) - -if __name__ == '__main__': - dev_mode = len(sys.argv) > 1 and sys.argv[1] == '--dev' - - if dev_mode: - APP = 'index:APP' - print(config) - print(device_config) - - uvicorn.run( - APP, - log_level=config['LOG_LEVEL'], - port=config['PORT'], - host=config['HOST'], - use_colors=dev_mode, - reload=dev_mode - ) diff --git a/whisper-service/model_bases/__init__.py b/whisper-service/model_bases/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/whisper-service/model_bases/buffer_audio_model_base.py b/whisper-service/model_bases/buffer_audio_model_base.py deleted file mode 100644 index 0de112c..0000000 --- a/whisper-service/model_bases/buffer_audio_model_base.py +++ /dev/null @@ -1,161 +0,0 @@ -''' -Provides a helper class for implementing Transcription Models - -Classes: - BufferAudioModelBase -''' -from abc import abstractmethod -import numpy as np -import numpy.typing as npt -from utils.config_dict_contains import config_dict_contains_int, config_dict_contains_float -from utils.decode_wav import decode_wav -from utils.np_circular_buffer import NPCircularBuffer -from model_bases.transcription_model_base import TranscriptionModelBase -from custom_types.config_types import ImplementationModelConfig - - -class BufferAudioModelBase(TranscriptionModelBase): - ''' - A partial TranscriptionModelBase implementation that handles buffering audio chunks - into larger segments. - - Audio chunks that are passed to queue_audio_chunk() are buffered until larger chunks - before process_segment() is called. - - Implements the queue_audio_chunk() method. The load_model(), unload_model(), - and process_segment() methods must be implemented. - ''' - __slots__ = ['max_segment_samples', 'min_new_samples', - 'num_last_processed_samples', 'num_purged_samples', 'buffer', 'silence_threshold'] - SAMPLE_RATE = 16_000 - - def __init__(self, ws, config): - ''' - Called when a websocket requests a transcription model. - - Parameters: - ws (WebSocket) : FastAPI websocket that requested the model - config (TranscriptionModelConfig): Custom JSON object containing configuration for model - Defined by implementation - ''' - super().__init__(ws, config) - self.max_segment_samples = config['max_segment_samples'] - self.min_new_samples = config['min_new_samples'] - self.silence_threshold = config['silence_threshold'] - - self.num_last_processed_samples = 0 - self.num_purged_samples = 0 - self.buffer = NPCircularBuffer( - self.max_segment_samples, - dtype=np.float32 - ) - - @staticmethod - def validate_config(config: dict) -> ImplementationModelConfig: - ''' - Should check if loaded JSON config is valid. Called model is instantiated. - Throw an error if provided config is not valid - Remember to call valididate_config for any model_bases to ensure configuration - for model_bases is checked as well. - e.g. if you use LocalAgreeModelBase: config = LocalAgreeModelBase.validate(config) - - Parameters: - config (dict): Parsed JSON config from server device_config.json. Guaranteed to be a dict. - - Returns: - config (TranscriptionModelConfig): Validated config object - ''' - config_dict_contains_int(config, 'min_new_samples') - config_dict_contains_int( - config, - 'max_segment_samples', - minimum=config['min_new_samples'] - ) - config_dict_contains_float(config, 'silence_threshold') - return config - - def load_model(self) -> None: - ''' - Should load model into memory to be ready for transcription. - Called when websocket connects. - ''' - raise NotImplementedError('Must implement per model') - - def unload_model(self) -> None: - ''' - Should unload model from memory and cleanup. - Called when websocket disconnects. - ''' - raise NotImplementedError('Must implement per model') - - @abstractmethod - async def process_segment( - self, - audio_segment: npt.NDArray, - audio_segment_start_time: float - ) -> int: - ''' - Called when an audio segment is ready to be transcribed. - To implement this function, the audio_segment should be transcribed - and on_final_transcript_block() or on_in_progress_transcript_block(). - - Note: Once audio_segment contains max_segment_samples, process_segment - must return a nonzero value. Otherwise, the buffer never shrinks - and process_segment could be called in an infinite loop - - Parameters: - audio_segment (1D numpy array): - Contains float16 audio normalized to [-1, 1] at 16k sample rate. - It contains at least min_new_segment new samples since previous call of process_segment. - It also contains at most max_segment_samples samples. - - audio_segment_start_time (float): - The timestamp of the start of the provided audio_segment calculated using the cumulative - return value of process_segment - - Returns: - Number samples of audio to purge from audio buffer. This value is also used to compute the - start time provided to process_segment. - ''' - raise NotImplementedError('Must implement per model') - - async def queue_audio_chunk(self, audio_chunk) -> None: - ''' - Called when an audio chunk is received. - - Buffers audio_chunks until there are at least min_new_samples more audio samples - in the buffer compared previous call of process_segment() before calling - process_segment() again to transcribe audio. Purges the designated number of samples - from buffer based on return value of process_segment. - - Parameters: - audio_chunk (io.BytesIO): A buffer containing wav audio - ''' - audio = decode_wav(audio_chunk) - - extra_audio = self.buffer.append_sequence(audio) - - # If buffer is full, process segments until entire audio chunk can be - # inserted into buffer - while len(extra_audio) > 0: - samples_to_purge = await self.process_segment( - self.buffer.get_curr_buffer().copy(), - self.num_purged_samples / self.SAMPLE_RATE - ) - - self.buffer.shift_buffer(samples_to_purge) - self.num_purged_samples += samples_to_purge - self.num_last_processed_samples = len(self.buffer) - - extra_audio = self.buffer.append_sequence(extra_audio) - - # Once there are enough new samples, process segments once - if (len(self.buffer) - self.num_last_processed_samples) > self.min_new_samples: - samples_to_purge = await self.process_segment( - self.buffer.get_curr_buffer(), - self.num_purged_samples / self.SAMPLE_RATE - ) - - self.buffer.shift_buffer(samples_to_purge) - self.num_purged_samples += samples_to_purge - self.num_last_processed_samples = len(self.buffer) diff --git a/whisper-service/model_bases/local_agree_model_base.py b/whisper-service/model_bases/local_agree_model_base.py deleted file mode 100644 index b68341d..0000000 --- a/whisper-service/model_bases/local_agree_model_base.py +++ /dev/null @@ -1,285 +0,0 @@ -''' -Provides a helper class for implementing Transcription Models - -Classes: - TranscriptionSegment - LocalAgreeModelBase -''' -from abc import abstractmethod -import math -import numpy.typing as npt -from model_bases.buffer_audio_model_base import BufferAudioModelBase -from utils.config_dict_contains import config_dict_contains_int -from custom_types.config_types import ImplementationModelConfig - - -class TranscriptionSegment: - ''' - Class for holding transcription chunks. - ''' - __slots__ = ['text', 'start', 'end'] - - def __init__(self, text: str, start: float, end: float): - ''' - Parameters: - text (str) : Transcribed text for chunk. - start (float): Start time of current transcription. (relative to first block) - end (float): End time of current transcription. - ''' - self.text = text - self.start = start - self.end = end - - def __repr__(self) -> str: - ''' - Returns: - String representation of TranscriptionSegment - ''' - return f'[{self.start:6.2f} - {self.end:6.2f}] {self.text}' - - def __str__(self) -> str: - ''' - Returns: - String representation of TranscriptionSegment - ''' - return f'[{self.start:6.2f} - {self.end:6.2f}] {self.text}' - - -class LocalAgreeModelBase(BufferAudioModelBase): - ''' - A partial BufferAudioModelBase implementation that handles local agreement - (Liu et al., 2020) (Macháček et al., 2023) - - If the last n transcriptions have matching prefix ending in sentence end punctuation, - that prefix is emitted as a finalized transcription. The audio samples corresponding - to that transcription is then purged from the buffer. Any remaining transcription text - is emitted as an in_progress transcription. - - Implements the process_segment() method. - The load_model(), unload_model(), and transcribe_audio() methods need to be implemented. - - @misc{liu2020lowlatencysequencetosequencespeechrecognition, - title={Low-Latency Sequence-to-Sequence Speech Recognition - and Translation by Partial Hypothesis Selection}, - author={Danni Liu and Gerasimos Spanakis and Jan Niehues}, - year={2020}, - eprint={2005.11185}, - archivePrefix={arXiv}, - primaryClass={cs.CL}, - url={https://arxiv.org/abs/2005.11185}, - } - @misc{macháček2023turningwhisperrealtimetranscription, - title={Turning Whisper into Real-Time Transcription System}, - author={Dominik Macháček and Raj Dabre and Ondřej Bojar}, - year={2023}, - eprint={2307.14743}, - archivePrefix={arXiv}, - primaryClass={cs.CL}, - url={https://arxiv.org/abs/2307.14743}, - } - ''' - __slots__ = ['prev_text', 'local_agree_dim', 'prev_transcriptions'] - - SENTENCE_ENDS = ('.', '?', '!') - SENTENCE_ENDS_WHITELIST = '...' - - def __init__(self, ws, *args, local_agree_dim=2, **kwargs): - ''' - Called when a websocket requests a transcription model. - - Parameters: - ws (WebSocket): FastAPI websocket that requested the model. - local_agree_dim (int) : Number of dimensions to run local agreement on. - *args, **kwargs : Args passed to BufferAudioModelBase - ''' - super().__init__(ws, *args, **kwargs) - - self.prev_text = '' - self.local_agree_dim = local_agree_dim - self.prev_transcriptions: list[list[TranscriptionSegment]] = [] - - @staticmethod - def validate_config(config: dict) -> ImplementationModelConfig: - ''' - Should check if loaded JSON config is valid. Called model is instantiated. - Throw an error if provided config is not valid - Remember to call valididate_config for any model_bases to ensure configuration - for model_bases is checked as well. - e.g. if you use LocalAgreeModelBase: config = LocalAgreeModelBase.validate(config) - - Parameters: - config (dict): Parsed JSON config from server device_config.json. Guaranteed to be a dict. - - Returns: - config (TranscriptionModelConfig): Validated config object - ''' - config = BufferAudioModelBase.validate_config(config) - config_dict_contains_int(config, 'local_agree_dim', minimum=1) - return config - - def load_model(self) -> None: - ''' - Should load model into memory to be ready for transcription. - Called when websocket connects. - ''' - raise NotImplementedError('Must implement per model') - - def unload_model(self) -> None: - ''' - Should unload model from memory and cleanup. - Called when websocket disconnects. - ''' - raise NotImplementedError('Must implement per model') - - @abstractmethod - async def transcribe_audio( - self, - audio_segment: npt.NDArray, - prev_text: str - ) -> list[TranscriptionSegment]: - ''' - Transcribe audio into TranscriptionSegments containing text, start, and end times - - Parameters: - audio_segment (1D numpy array): - Contains float16 audio normalized to [-1, 1] at 16k sample rate. - - prev_text (str): - The previously finalized text that occurred before the current audio_segment. - Used to precondition model for accuracy. - - Returns: - A list of TranscriptionSegments - ''' - raise NotImplementedError('Must implement per model') - - async def process_segment(self, audio_segment, audio_segment_start_time): - ''' - Called when an audio segment is ready to be transcribed. - - Implements local agreement described in referenced papers. - Calls transcribe_audio() to get TranscriptionSegments from audio segments. - - Parameters: - audio_segment (1D numpy array): - Contains float16 audio normalized to [-1, 1] at 16k sample rate. - It contains at least min_new_segment new samples since previous call of process_segment. - It also contains at most max_segment_samples samples. - - audio_segment_start_time (float): - The timestamp of the start of the provided audio_segment calculated using the cumulative - return value of process_segment - - Returns: - Number samples of audio to purge from audio buffer. This value is also used to compute the - start time provided to process_segment. - ''' - - max_segment_length_reached = len( - audio_segment) >= self.max_segment_samples - - segments = await self.transcribe_audio(audio_segment, self.prev_text) - - # Extract segments that satisfy local agreement - final_text = '' - final_end_idx = 0 - final_end_time = 0 - for i in range(min(len(segments), self.max_local_agree_length())): - if not self.local_agree(segments[i], i): - break - final_text += segments[i].text - - if ( - final_text.endswith(self.SENTENCE_ENDS) and - not final_text.endswith(self.SENTENCE_ENDS_WHITELIST) - ): - start = final_end_time - final_end_time = max(final_end_time, segments[i].end) - - self.prev_text = final_text - await self.on_final_transcript_block( - final_text, - audio_segment_start_time + start, - audio_segment_start_time + final_end_time - ) - final_end_idx = i + 1 - final_text = '' - - # If max segment length has been reached, force finalization of some text - if max_segment_length_reached: - start = final_end_time - forced_final_text = '' - while ( - final_end_idx < len(segments) and - final_end_time < self.min_new_samples / self.SAMPLE_RATE - ): - forced_final_text += segments[final_end_idx].text - - final_end_time = max( - final_end_time, - segments[final_end_idx].end - ) - final_end_idx += 1 - self.prev_text = forced_final_text - - await self.on_final_transcript_block( - forced_final_text, - audio_segment_start_time + start, - audio_segment_start_time + final_end_time - ) - - # Output remaining text as in progress transcription - in_progress = '' - in_progress_end_time = final_end_time - for i in range(final_end_idx, len(segments)): - in_progress += segments[i].text - in_progress_end_time = max(in_progress_end_time, segments[i].end) - await self.on_in_progress_transcript_block( - in_progress, - audio_segment_start_time + final_end_time, - audio_segment_start_time + in_progress_end_time - ) - - # Update transcription history - self.prev_transcriptions.append(segments) - if len(self.prev_transcriptions) >= self.local_agree_dim: - self.prev_transcriptions.pop(0) - - finalized_samples = int(final_end_time * self.SAMPLE_RATE) - if max_segment_length_reached: - # Ensure at least at least the minimum number of samples is purged in case of silence - # where no forced finalized text would be emitted. - finalized_samples = max(self.min_new_samples, finalized_samples) - return min(finalized_samples, len(audio_segment)) - - def local_agree(self, segment: TranscriptionSegment, index: int) -> bool: - ''' - Checks if segment at given index is in local agreement with transcription history. - - Parameters: - segment (TranscriptionSegment): New TranscriptionSegment to compare with history. - index (int) : Position TranscriptSegment corresponds to in history. - - Returns: - True if in local agreement, False otherwise - ''' - # Reject if there is not enough history to form required agreement - if len(self.prev_transcriptions) != self.local_agree_dim - 1: - return False - - # Reject if any transcription doesn't match - for transcription in self.prev_transcriptions: - if segment.text != transcription[index].text: - return False - - return True - - def max_local_agree_length(self) -> int: - ''' - Returns: - The shortest transcription history sequence. This is also the maximum number of - transcription segments that could possibly agree in required dimensions. - ''' - if len(self.prev_transcriptions) == 0: - return math.inf - return min(len(transcription) for transcription in self.prev_transcriptions) diff --git a/whisper-service/model_bases/transcription_model_base.py b/whisper-service/model_bases/transcription_model_base.py deleted file mode 100644 index a7d8abb..0000000 --- a/whisper-service/model_bases/transcription_model_base.py +++ /dev/null @@ -1,126 +0,0 @@ -''' -Provides classes to define a unified transcription model interface. - -Classes: - BackendTranscriptionBlockType - TranscriptionModelBase - -Types: - TranscriptionModelConfig -''' -import io -import logging -from abc import ABC, abstractmethod -from fastapi import WebSocket -from custom_types.config_types import ImplementationModelConfig -from custom_types.transcription_types import BackendTranscriptionBlockType, BackendTranscriptBlock - - -class TranscriptionModelBase(ABC): - ''' - Base transcription model class. - Presents a unified interface for using different transcription models on the backend. - - The validate_config(), load_model(), unload_model(), and - queue_audio_chunk() methods must be implemented. - ''' - __slots__ = ['logger', 'ws', 'config'] - - def __init__(self, ws: WebSocket, config: ImplementationModelConfig): - ''' - Called when a websocket requests a transcription model. - - Parameters: - ws (WebSocket): FastAPI websocket that requested the model - config (TranscriptionModelConfig): Custom JSON object containing configuration for model - Defined by implementation - ''' - self.ws = ws - self.config = self.validate_config(config) - self.logger = logging.getLogger('uvicorn.error') - - @staticmethod - @abstractmethod - def validate_config(config: dict) -> ImplementationModelConfig: - ''' - Should check if loaded JSON config is valid. Called model is instantiated. - Throw an error if provided config is not valid - Remember to call valididate_config for any model_bases to ensure configuration - for model_bases is checked as well. - e.g. if you use LocalAgreeModelBase: config = LocalAgreeModelBase.validate(config) - - Parameters: - config (dict): Parsed JSON config from server device_config.json. Guaranteed to be a dict. - - Returns: - config (TranscriptionModelConfig): Validated config object - ''' - raise NotImplementedError('Must implement per model') - - @abstractmethod - def load_model(self) -> None: - ''' - Should load model into memory to be ready for transcription. - Called when websocket connects. - ''' - raise NotImplementedError('Must implement per model') - - @abstractmethod - def unload_model(self) -> None: - ''' - Should unload model from memory and cleanup. - Called when websocket disconnects. - ''' - raise NotImplementedError('Must implement per model') - - @abstractmethod - async def queue_audio_chunk(self, audio_chunk: io.BytesIO) -> None: - ''' - Called when an audio chunk is received. - - To implement this function, the audio_chunk should be used to produce transcriptions. - This may require buffering the chunks until a larger segment is ready. Once a transcription - is ready, call on_final_transcript_block() or on_in_progress_transcript_block(). - - Parameters: - audio_chunk (io.BytesIO): A buffer containing wav audio - ''' - raise NotImplementedError('Must implement per model') - - async def on_final_transcript_block(self, text: str, start=-1.0, end=-1.0) -> None: - ''' - Call this when a block of finalized transcription is ready - - Parameters: - text (str) : Finalized transcribed text - start (float): Start time of this transcription chunk [Optional] - end (float): End time of this transcription chunk [Optional] - ''' - self.logger.info('[%6.2f - %6.2f] Final : %s', start, end, text) - transcript_block: BackendTranscriptBlock = { - 'type': BackendTranscriptionBlockType.FINAL, - 'text': text, - 'start': start, - 'end': end - } - await self.ws.send_json(transcript_block) - - async def on_in_progress_transcript_block(self, text: str, start=-1.0, end=-1.0) -> None: - ''' - Call this when a block of in progress transcription is ready. - This is used to provide a lower latency transcription at the cost of some accuracy. - If model does not support in progress guesses, only call on_final_transcript_chunk(). - - Parameters: - text (str) : Finalized transcribed text - start (float): Start time of this transcription block [Optional] - end (float): End time of this transcription block [Optional] - ''' - self.logger.info('[%6.2f - %6.2f] In Progress: %s', start, end, text) - transcript_block: BackendTranscriptBlock = { - 'type': BackendTranscriptionBlockType.IN_PROGRESS, - 'text': text, - 'start': start, - 'end': end - } - await self.ws.send_json(transcript_block) diff --git a/whisper-service/model_bases/transcription_model_base_test.py b/whisper-service/model_bases/transcription_model_base_test.py deleted file mode 100644 index 55d15a0..0000000 --- a/whisper-service/model_bases/transcription_model_base_test.py +++ /dev/null @@ -1,122 +0,0 @@ -''' -Unit tests for TranscriptionModelBase class -''' -# pylint: disable=redefined-outer-name -import pytest -from model_bases.transcription_model_base import TranscriptionModelBase -from custom_types.transcription_types import BackendTranscriptionBlockType - -fake_config = { - 'some_param': 'string', - 'another_param': 0, - 'nested_param': { - 'array_param': ['erased'] - } -} -returned_fake_config = { - 'some_param': 'str', - 'another_param': 1, - 'nested_param': { - 'array_param': [] - } -} - - -class FakeWebSocket: - ''' - Simple fake websocket to capture what send_json is called with. - ''' - - def __init__(self): - self.sent_messages = [] - - async def send_json(self, message): - ''' - Records what send_json() is called with - ''' - self.sent_messages.append(message) - - def get_sent_messages(self): - ''' - Get record of what send_json() was called with - ''' - return self.sent_messages - - -@pytest.fixture(scope='function') -def fake_implementation(): - ''' - Create a fake transcription model for each test - ''' - class Fake(TranscriptionModelBase): - ''' - Fake transcription model to track how object's methods are called - ''' - @staticmethod - def validate_config(config): - return returned_fake_config - - def load_model(self): - return None - - def unload_model(self): - return None - - async def queue_audio_chunk(self, audio_chunk): - return None - - return Fake - - -@pytest.mark.asyncio -async def test_on_final_transcript(fake_implementation): - ''' - Test that on_final_transcript_block() sends correct websocket message - ''' - fake_ws = FakeWebSocket() - model_base = fake_implementation(fake_ws, fake_config) - - await model_base.on_final_transcript_block("Hello world", start=0, end=1) - - assert len(fake_ws.get_sent_messages()) == 1 - message = fake_ws.get_sent_messages()[0] - assert message['type'] == BackendTranscriptionBlockType.FINAL - assert message['text'] == "Hello world" - assert message['start'] == 0 - assert message['end'] == 1 - - -@pytest.mark.asyncio -async def test_on_in_progress_transcript(fake_implementation): - ''' - Test that on_in_progress_transcript_block() sends correct websocket message - ''' - fake_ws = FakeWebSocket() - model_base = fake_implementation(fake_ws, fake_config) - - await model_base.on_in_progress_transcript_block("Processing...", start=0, end=1) - - assert len(fake_ws.get_sent_messages()) == 1 - message = fake_ws.get_sent_messages()[0] - assert message['type'] == BackendTranscriptionBlockType.IN_PROGRESS - assert message['text'] == "Processing..." - assert message['start'] == 0 - assert message['end'] == 1 - - -def test_validate_config_called(fake_implementation): - ''' - Test that validate_config() is called when model is instantiated and - return value is set as config property - ''' - fake_config = { - 'some_param': 'str', - 'another_param': 1, - 'nested_param': { - 'array_param': [] - } - } - fake_ws = FakeWebSocket() - model = fake_implementation(fake_ws, fake_config) - - assert model.config == returned_fake_config, 'config property not set' diff --git a/whisper-service/model_implementations/faster_whisper_model.py b/whisper-service/model_implementations/faster_whisper_model.py deleted file mode 100644 index 5d424a2..0000000 --- a/whisper-service/model_implementations/faster_whisper_model.py +++ /dev/null @@ -1,93 +0,0 @@ -''' -Implementation of TranscriptionModelBase using faster_whisper - -Classes: - FasterWhisperModel -''' -from faster_whisper import WhisperModel -from model_bases.local_agree_model_base import LocalAgreeModelBase, TranscriptionSegment - - -class FasterWhisperModel(LocalAgreeModelBase): - ''' - Implementation of TranscriptionModelBase using faster whisper and local agreement. - ''' - __slots__ = ['model'] - - def __init__(self, ws, config): - ''' - Called when a websocket requests a transcription model. - - Parameters: - ws (WebSocket) : FastAPI websocket that requested the model - config (TranscriptionModelConfig): Custom JSON object containing configuration for model - Defined by implementation - ''' - super().__init__(ws, config) - self.model = None - - @staticmethod - def validate_config(config): - ''' - Should check if loaded JSON config is valid. Called model is instantiated. - Throw an error if provided config is not valid - Remember to call valididate_config for any model_bases to ensure configuration - for model_bases is checked as well. - e.g. if you use LocalAgreeModelBase: config = LocalAgreeModelBase.validate(config) - - Parameters: - config (dict): Parsed JSON config from server device_config.json. Guaranteed to be a dict. - - Returns: - config (TranscriptionModelConfig): Validated config object - ''' - config = LocalAgreeModelBase.validate_config(config) - return config - - def load_model(self): - ''' - Loads model into memory to be ready for transcription. - Called when websocket connects. - ''' - self.model = WhisperModel( - self.config['model'], - device=self.config['device'] - ) - - def unload_model(self): - ''' - Unloads model from memory and cleans up. - Called when websocket disconnects. - ''' - if self.model and self.model.model.model_is_loaded: - self.model.model.unload_model() - - async def transcribe_audio(self, audio_segment, prev_text): - ''' - Transcribes audio into TranscriptionSegments containing text, start, and end times - - Parameters: - audio_segment (1D numpy array): - Contains float16 audio normalized to [-1, 1] at 16k sample rate. - - prev_text (str): - The previously finalized text that occurred before the current audio_segment. - Used to precondition model for accuracy. - - Returns: - A list of TranscriptionSegments - ''' - transcription, _ = self.model.transcribe( - audio_segment, - initial_prompt=prev_text, - word_timestamps=True, - vad_filter=True - ) - - segments = [] - for part in transcription: - for word in part.words: - segments.append( - TranscriptionSegment(word.word, word.start, word.end) - ) - return segments diff --git a/whisper-service/model_implementations/import_model_implementation.py b/whisper-service/model_implementations/import_model_implementation.py deleted file mode 100644 index e71e81f..0000000 --- a/whisper-service/model_implementations/import_model_implementation.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Function for importing specified model implementation - -Functions: - import_model_implementation -''' -# pylint: disable=import-outside-toplevel -# Disable linter rule so we can do imports in import_model_implementation -# Avoids needing to import every model even if unused -# Can potentially help with dealing with conflicting dependencies) -from model_bases.transcription_model_base import TranscriptionModelBase -from custom_types.config_types import ModelImplementationId - - -def import_model_implementation( - model_implementation_id: ModelImplementationId -) -> TranscriptionModelBase: - ''' - Imports model with corresponding model_implementation_id. - - Parameters: - model_implementation_id (str): Unique identifier for model to instantiate - - Returns: - A TranscriptionModelBase class - ''' - match(model_implementation_id): - case ModelImplementationId.MOCK_TRANSCRIPTION_DURATION: - from model_implementations.mock_transcription_duration import MockTranscribeDuration - return MockTranscribeDuration - case ModelImplementationId.FASTER_WHISPER: - from model_implementations.faster_whisper_model import FasterWhisperModel - return FasterWhisperModel - case _: - raise KeyError( - f'No model implementation matching {model_implementation_id}' - ) diff --git a/whisper-service/model_implementations/mock_transcription_duration.py b/whisper-service/model_implementations/mock_transcription_duration.py deleted file mode 100644 index 502b8d3..0000000 --- a/whisper-service/model_implementations/mock_transcription_duration.py +++ /dev/null @@ -1,69 +0,0 @@ -''' -Mock implementation of TranscriptionModelBase that returns duration of received audio chunks. - -Classes: - MockTranscribeDuration -''' -import wave -from model_bases.transcription_model_base import TranscriptionModelBase - - -class MockTranscribeDuration(TranscriptionModelBase): - ''' - Dummy TranscriptionModelBase implementation that returns the - duration of recieved audio as "transcription" - ''' - time = 0 - - @staticmethod - def validate_config(config): - ''' - Should check if loaded JSON config is valid. Called model is instantiated. - Throw an error if provided config is not valid - Remember to call valididate_config for any model_bases to ensure configuration - for model_bases is checked as well. - e.g. if you use LocalAgreeModelBase: config = LocalAgreeModelBase.validate(config) - - Parameters: - config (dict): Parsed JSON config from server device_config.json. Guaranteed to be a dict. - - Returns: - config (TranscriptionModelConfig): Validated config object - ''' - return config - - def load_model(self): - ''' - Loads model into memory to be ready for transcription. - Called when websocket connects. - ''' - - def unload_model(self): - ''' - Unloads model from memory and cleans up. - Called when websocket disconnects. - ''' - - async def queue_audio_chunk(self, audio_chunk): - ''' - Called when an audio chunk is received. - - Generates final transcription blocks containing duration of audio received. - - Parameters: - audio_chunk (io.BytesIO): A buffer containing wav audio - ''' - infofile = wave.open(audio_chunk, 'r') - frames = infofile.getnframes() - rate = infofile.getframerate() - - duration = frames / float(rate) - - start = self.time - self.time += duration - - await self.on_final_transcript_block( - f'Received {duration} seconds of audio.', - start, - self.time - ) diff --git a/whisper-service/requirements.txt b/whisper-service/requirements.txt deleted file mode 100644 index 5a60801..0000000 --- a/whisper-service/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -fastapi==0.115.8 -fastapi-cli===0.0.7 -websockets==15.0 -numpy==2.2.4 - -# Test requirements -pytest==8.3.5 -pytest-cov==6.1.1 -pylint==3.3.6 -httpx==0.28.1 -pytest-asyncio==0.25.3 -pytest-mock==3.14.0 - -# faster-whisper models -faster-whisper==1.1.1 -ctranslate2==4.4.0 diff --git a/whisper-service/server/create_server.py b/whisper-service/server/create_server.py deleted file mode 100644 index ed0dec6..0000000 --- a/whisper-service/server/create_server.py +++ /dev/null @@ -1,90 +0,0 @@ -''' -Function to instantiate FastAPI webserver. -Creates executes the function if this file is run directly using the fastapi CLI. - -Functions: - create_server -''' -# pylint: disable=too-many-arguments,too-many-positional-arguments -import io -from typing import Callable, Type, Literal -from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from model_bases.transcription_model_base import TranscriptionModelBase -from custom_types.config_types import AppConfig, DeviceConfig, ModelImplementationId -from custom_types.model_selection_types import SelectionOptions, SelectedOption - - -def create_server( - config: AppConfig, - device_config: DeviceConfig, - selection_options: SelectionOptions, - import_implementation_fun: Callable[[ModelImplementationId], Type[TranscriptionModelBase]], - authenticate_websocket_fun: Callable[[WebSocket, AppConfig], bool], - select_model_fun: Callable[ - [WebSocket, DeviceConfig, SelectionOptions], SelectedOption | Literal[False] - ] -) -> FastAPI: - ''' - Instanciates FastAPI webserver. - - Parameters: - config (AppConfig): Application configuration object - device_config (DeviceConfig): Application device configuration object - selection_options (SelectionOptions): Available selection options to send to frontend - import_implementation_fun (function): Function that takes in a modelKey and a WebSocket and - returns the corresponding model implementation class - authenticate_websocket_fun (function): Function that authenticates a websocket - returns True is successful, False otherwise - select_model_fun (function): Function that get model selection from websocket - returns received selection, False on error - - Returns: - FastAPI webserver - ''' - fastapi_app = FastAPI() - - @fastapi_app.websocket("/sourcesink") - async def sourcesink(websocket: WebSocket): - ''' - Parameters: - api_key (str): Secret API key passed in through URL query parameters - ''' - await websocket.accept() - - if not await authenticate_websocket_fun(websocket, config): - return await websocket.close() - - selected_option = await select_model_fun(websocket, device_config, selection_options) - if not selected_option: - return await websocket.close() - - model_key = selected_option['model_key'] - - # Create and setup requested model - model_config = device_config[model_key] - implementation = import_implementation_fun( - model_config['implementation_id'] - ) - transcription_model = implementation( - websocket, - model_config['implementation_configuration'] - ) - transcription_model.load_model() - - # Send any audio chunks to transcription model - while True: - try: - data = await websocket.receive_bytes() - await transcription_model.queue_audio_chunk(io.BytesIO(data)) - except WebSocketDisconnect: - transcription_model.unload_model() - return - - @fastapi_app.get("/healthcheck") - def healthcheck(): - ''' - Simple healthcheck endpoint to see if server is alive - ''' - return 'ok' - - return fastapi_app diff --git a/whisper-service/server/create_server_test.py b/whisper-service/server/create_server_test.py deleted file mode 100644 index 93a0448..0000000 --- a/whisper-service/server/create_server_test.py +++ /dev/null @@ -1,157 +0,0 @@ -''' -Unit tests for create_server function. -''' -# pylint: disable=redefined-outer-name,too-many-locals,unused-argument -import os -from pytest_mock import MockerFixture -from fastapi import WebSocket -from fastapi.testclient import TestClient -from app_config.load_config import AppConfig -from model_bases.transcription_model_base import TranscriptionModelBase -from server.create_server import create_server - - -# Load some test files to send through websocket -wav_files = [ - "../../test-audio-files/wikipedia-.fun/chunked/chunk_000.wav", - "../../test-audio-files/wikipedia-.fun/chunked/chunk_001.wav", - "../../test-audio-files/wikipedia-.fun/chunked/chunk_002.wav", - "../../test-audio-files/wikipedia-.fun/chunked/chunk_003.wav", - "../../test-audio-files/wikipedia-.fun/chunked/chunk_004.wav", - "../../test-audio-files/wikipedia-.fun/chunked/chunk_005.wav", - "../../test-audio-files/wikipedia-.fun/chunked/chunk_006.wav", - "../../test-audio-files/wikipedia-.fun/chunked/chunk_007.wav", -] -wav_data = [] -for wav_file in wav_files: - file_path = os.path.join(os.path.dirname(__file__), wav_file) - assert os.path.exists(file_path), "Test wav file not found" - - with open(file_path, "rb") as f: - wav_data.append(f.read()) - - -fake_config = AppConfig() -fake_config['API_KEY'] = 'SOME_API_KEY' -fake_config['LOG_LEVEL'] = 'info' -fake_config['PORT'] = -1 -fake_config['HOST'] = '127.0.0.1' - -fake_device_config = { - 'model_key_1': { - 'display_name': 'Model Name', - 'description': 'Some description', - 'implementation_id': 'implementation_id_1', - 'implementation_configuration': { - 'some': 'config', - 'key': 10, - }, - 'available_features': {} - }, -} - -fake_selection_options = [{ - 'model_key': 'model_key_1' -}] - - -class FakeModelImplementation(TranscriptionModelBase): - ''' - Create a fake transcription model implementation for each test - ''' - @staticmethod - def validate_config(config): - return config - - def load_model(self): - return None - - def unload_model(self): - return None - - async def queue_audio_chunk(self, audio_chunk): - return None - - -def import_fun(key): - ''' - Fake import function to return FakeModelImplementation - ''' - if key == 'implementation_id_1': - return FakeModelImplementation - raise KeyError('Invalid Key') - - -async def auth_fun(*args): - ''' - Fake authenticate websocket function to skip authentication - ''' - return True - - -async def select_model(*args): - ''' - Fake select model function to skip model selection - ''' - return { - 'model_key': 'model_key_1', - 'feature_selection': {} - } - - -def test_loads_unloads_model(mocker: MockerFixture,): - ''' - Test that websocket handler instanciates and loads model correctly - ''' - init_spy = mocker.spy(FakeModelImplementation, '__init__') - load_spy = mocker.spy(FakeModelImplementation, 'load_model') - unload_spy = mocker.spy(FakeModelImplementation, 'unload_model') - - app = create_server( - fake_config, - fake_device_config, - fake_selection_options, - import_fun, - auth_fun, - select_model - ) - test_client = TestClient(app) - - with test_client.websocket_connect("/sourcesink") as websocket: - websocket.close() - - load_spy.assert_called_once() - unload_spy.assert_called_once() - - assert init_spy.call_args_list[0].args[1].__class__ == WebSocket, \ - 'Model initialized with websocket' - assert init_spy.call_args_list[0].args[2] == \ - fake_device_config['model_key_1']['implementation_configuration'], \ - 'Model initinalized with implementation config' - - -def test_queues_audio_chunks(mocker: MockerFixture,): - ''' - Test that websocket handler instanciates and loads model correctly - ''' - queue_spy = mocker.spy(FakeModelImplementation, 'queue_audio_chunk') - - app = create_server( - fake_config, - fake_device_config, - fake_selection_options, - import_fun, - auth_fun, - select_model - ) - test_client = TestClient(app) - - with test_client.websocket_connect("/sourcesink") as websocket: - for wav in wav_data: - websocket.send_bytes(wav) - - assert queue_spy.call_count == len(wav_data), \ - "queue_audio_chunk called correct number of times" - for i, data in enumerate(wav_data): - assert queue_spy.call_args_list[i].args[1].getvalue() == data, \ - "Correct data transferred" diff --git a/whisper-service/server/helpers/authenticate_websocket.py b/whisper-service/server/helpers/authenticate_websocket.py deleted file mode 100644 index ccf7b8f..0000000 --- a/whisper-service/server/helpers/authenticate_websocket.py +++ /dev/null @@ -1,58 +0,0 @@ -''' -Helper function to simplify authenticating websocket connections - -Functions: - authenticate_websocket -''' -import json -import logging -import asyncio -from fastapi import WebSocket, WebSocketDisconnect -from custom_types.config_types import AppConfig -from custom_types.authentication_types import WhisperAuthMessage -from server.helpers.receive_json_timeout import receive_json_timeout - - -async def authenticate_websocket(websocket: WebSocket, config: AppConfig) -> bool: - ''' - Helper function to authenticate a new websocket - - Parameters: - websocket (WebSocket): Opened FastAPI websocket - config (AppConfig): Application configuration object - - Returns: - True is successfully authenticated, False otherwise - ''' - logger = logging.getLogger('uvicorn.error') - try: - auth_message: WhisperAuthMessage = await receive_json_timeout(websocket) - except json.JSONDecodeError: - logger.info( - 'Authentication Failed: Invalid authentication message') - await websocket.send_json({ - 'error': True, - 'msg': 'Authentication Failed: Invalid authentication message' - }) - return False - except asyncio.TimeoutError: - logger.info('Authentication Timeout: No api_key received in time') - await websocket.send_json({ - 'error': True, - 'msg': 'Authentication Timeout: No api_key received in time' - }) - return False - except WebSocketDisconnect: - logger.info('Authentication Failed: Websocket closed') - return False - - # Reject invalid API keys - if 'api_key' not in auth_message or auth_message['api_key'] != config['API_KEY']: - logger.info('Authentication Failed: Invalid key') - await websocket.send_json({ - 'error': True, - 'msg': 'Authentication Failed: Invalid key' - }) - return False - - return True diff --git a/whisper-service/server/helpers/receive_json_timeout.py b/whisper-service/server/helpers/receive_json_timeout.py deleted file mode 100644 index 5609955..0000000 --- a/whisper-service/server/helpers/receive_json_timeout.py +++ /dev/null @@ -1,27 +0,0 @@ -''' -Helper function for receving JSON data from a websocket with a timeout - -Functions: - receive_json_timeout -''' -import json -import asyncio -from fastapi import WebSocket - - -async def receive_json_timeout(websocket: WebSocket): - ''' - Helper function for receving JSON data from a websocket with a timeout - - Parameters: - websocket (WebSocket) : Opened FastAPI websocket - - Returns: - Parsed JSON object - ''' - return json.loads( - await asyncio.wait_for( - websocket.receive_text(), - timeout=5 - ) - ) diff --git a/whisper-service/server/helpers/select_model.py b/whisper-service/server/helpers/select_model.py deleted file mode 100644 index 0d7948a..0000000 --- a/whisper-service/server/helpers/select_model.py +++ /dev/null @@ -1,75 +0,0 @@ -''' -Helper function to simplify model selection over a websocket connection - -Functions: - select_model -''' -import json -import asyncio -import logging -from typing import Literal -from fastapi import WebSocket, WebSocketDisconnect -from custom_types.config_types import DeviceConfig -from custom_types.model_selection_types import SelectionOptions, SelectedOption -from server.helpers.receive_json_timeout import receive_json_timeout - - -async def select_model( - websocket: WebSocket, - device_config: DeviceConfig, - selection_options: SelectionOptions -) -> SelectedOption | Literal[False]: - ''' - Helper function to get model selection from a new websocket - - Parameters: - websocket (WebSocket) : Opened FastAPI websocket - device_config (DeviceConfig) : Application device configuration object - selection_options (SelectionOptions): Available selection options to send to frontend - - Returns: - SelectOption is successfully parsed selection, False otherwise - ''' - logger = logging.getLogger('uvicorn.error') - - await websocket.send_json(selection_options) - - try: - model_selection: SelectedOption = await receive_json_timeout(websocket) - except json.JSONDecodeError: - logger.info( - 'Model Selection Failed: Invalid model selection message') - await websocket.send_json({ - 'error': True, - 'msg': 'Model Selection Failed: Invalid model selection message' - }) - return False - except asyncio.TimeoutError: - logger.info( - 'Model Selection Timeout: No selection received in time') - await websocket.send_json({ - 'error': True, - 'msg': 'Model Selection Timeout: No selection received in time' - }) - return False - except WebSocketDisconnect: - logger.info('Model Selection Failed: Websocket closed') - return False - - if 'model_key' not in model_selection: - logger.info('Model Selection Failed: No model_key provided') - await websocket.send_json({ - 'error': True, - 'msg': 'Model Selection Failed: No model_key provided' - }) - return False - - if model_selection['model_key'] not in device_config: - logger.info('Model Selection Failed: Invalid model_key provided') - await websocket.send_json({ - 'error': True, - 'msg': 'Model Selection Failed: Invalid model_key provided' - }) - return False - - return model_selection diff --git a/whisper-service/template.env b/whisper-service/template.env deleted file mode 100644 index 03b1000..0000000 --- a/whisper-service/template.env +++ /dev/null @@ -1,8 +0,0 @@ -LOG_LEVEL=info - -#### API key required to establish websocket connection -API_KEY=CHANGEME - -#### Host and port websocket API should listen on -HOST=0.0.0.0 -PORT=8000 \ No newline at end of file diff --git a/whisper-service/utils/__init__.py b/whisper-service/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/whisper-service/utils/config_dict_contains.py b/whisper-service/utils/config_dict_contains.py deleted file mode 100644 index dc8c1c4..0000000 --- a/whisper-service/utils/config_dict_contains.py +++ /dev/null @@ -1,138 +0,0 @@ -''' -Utility functions to help when implementing TranscriptionModelBase.validate_config() - -Functions: - config_dict_contains_int - config_dict_contains_str - config_dict_contains_dict - config_dict_contains_list - config_dict_contains_one_of -''' -import sys -from typing import Any - - -def config_dict_contains_int(config: dict, key: str, minimum=-sys.maxsize - 1, maximum=sys.maxsize): - ''' - Checks if config contains a property, key, - that is an integer between minimum and maximum inclusive - - Parameters: - config (dict): Config dictionary - key (str) : Key to check in config dictionary - minimum (int) : (Optional) minimum value key is allowed to be - maximum (int) : (Optional) maximum value key is allowed to be - ''' - if key not in config: - raise ValueError(f'Config missing "{key}" property') - if not isinstance(config[key], int): - raise ValueError(f'"{key}" property of config must be an integer') - if config[key] < minimum: - raise ValueError( - f'{key} property of config must be greater than or equal to {minimum}' - ) - if config[key] > maximum: - raise ValueError( - f'{key} property of config must be less than or equal to {maximum}' - ) - - -def config_dict_contains_float( - config: dict, - key: str, - minimum=-sys.maxsize - 1, - maximum=sys.maxsize -): - ''' - Checks if config contains a property, key, - that is a float between minimum and maximum inclusive - - Parameters: - config (dict) : Config dictionary - key (str) : Key to check in config dictionary - minimum (float): (Optional) minimum value key is allowed to be - maximum (int) : (Optional) maximum value key is allowed to be - ''' - if key not in config: - raise ValueError(f'Config missing "{key}" property') - if not isinstance(config[key], float): - raise ValueError(f'"{key}" property of config must be a float') - if config[key] < minimum: - raise ValueError( - f'{key} property of config must be greater than or equal to {minimum}' - ) - if config[key] > maximum: - raise ValueError( - f'{key} property of config must be less than or equal to {maximum}' - ) - - -def config_dict_contains_str(config: dict, key: str, min_length=0, max_length=sys.maxsize): - ''' - Checks if config contains a property, key, - that is a string with length between min_length and max_length inclusive - - Parameters: - config (dict) : Config dictionary - key (str) : Key to check in config dictionary - min_length (int): (Optional) minimum value key is allowed to be - min_length (int): (Optional) maximum value key is allowed to be - ''' - if key not in config: - raise ValueError(f'Config missing "{key}" property') - if not isinstance(config[key], str): - raise ValueError(f'"{key}" property of config must be an string') - if len(config[key]) < min_length: - raise ValueError( - f'{key} property of config must be string with length \ -greater than or equal to {min_length}' - ) - if len(config[key]) > max_length: - raise ValueError( - f'{key} property of config must be string with length \ -less than or equal to {max_length}' - ) - - -def config_dict_contains_dict(config: dict, key: str): - ''' - Checks if config contains a property, key, that is a dict - - Parameters: - config (dict): Config dictionary - key (str) : Key to check in config dictionary - ''' - if key not in config: - raise ValueError(f'Config missing "{key}" property') - if not isinstance(config[key], dict): - raise ValueError(f'"{key}" property of config must be an object') - - -def config_dict_contains_list(config: dict, key: Any): - ''' - Checks if config contains a property, key, that is a list - - Parameters: - config (dict): Config dictionary - key (str) : Key to check in config dictionary - ''' - if key not in config: - raise ValueError(f'Config missing "{key}" property') - if not isinstance(config[key], list): - raise ValueError(f'"{key}" property of config must be an array') - - -def config_dict_contains_one_of(config: dict, key: Any, options: list[Any]): - ''' - Checks if config contains a property, key, that one of the options provided - - Parameters: - config (dict): Config dictionary - key (Any) : Key to check in config dictionary - options (list): List of possible options property can have - ''' - if key not in config: - raise ValueError(f'Config missing "{key}" property') - if config[key] not in options: - raise ValueError( - f'"{key}" property of config must be one of: {options}') diff --git a/whisper-service/utils/decode_wav.py b/whisper-service/utils/decode_wav.py deleted file mode 100644 index 46f9855..0000000 --- a/whisper-service/utils/decode_wav.py +++ /dev/null @@ -1,40 +0,0 @@ -''' -A utility function to help convert wav audio bytes to numpy array - -Functions: - decode_wav -''' -import io -import wave -import numpy as np -import numpy.typing as npt - - -def decode_wav(wav_buffer: io.BytesIO) -> npt.NDArray: - ''' - Decode a buffer containing wav data into numpy array for use with whisper. - Note: This function doesn't do any reencoding. - - Parameters: - wav_buffer (io.BytesIO): Wav audio buffer in the following format: - sample width : 2 bytes - sample rate : 16 khz - num channels : 1 - - Returns: - 1D numpy array containing float16 data normalized to [-1, 1]. - Array represents audio in single channel with 16_000 samples per second. - ''' - with wave.open(wav_buffer, 'rb') as wav_audio: - assert wav_audio.getsampwidth() == 2 - assert wav_audio.getframerate() == 16_000 - assert wav_audio.getnchannels() == 1 - - # Extract pcm audio data from wav - nframes = wav_audio.getnframes() - frames = wav_audio.readframes(nframes) - audio = np.frombuffer(frames, dtype=np.int16) - - # Normalize audio to between -1 and 1 - audio = audio.astype(np.float16) / abs(np.iinfo(np.int16).min) - return audio diff --git a/whisper-service/utils/np_circular_buffer.py b/whisper-service/utils/np_circular_buffer.py deleted file mode 100644 index 6181edb..0000000 --- a/whisper-service/utils/np_circular_buffer.py +++ /dev/null @@ -1,73 +0,0 @@ -''' -A utility class for buffering numpy arrays - -Classes: - NPCircularBuffer -''' -import numpy as np -import numpy.typing as npt - - -class NPCircularBuffer: - ''' - An implementation of a fixed length circular buffer numpy array. - ''' - __slots__ = ['dtype', 'max_size', 'array', 'end'] - - def __init__(self, max_size: int, dtype: npt.DTypeLike = 'int'): - ''' - Parameters: - max_size (int) : Maximum number of elements circular buffer should hold - dtype (numpy dtype): Data type of elements to place in buffer - ''' - self.dtype = dtype - self.max_size = max_size - - self.array = np.empty((max_size), dtype=self.dtype) - self.end = 0 - - def append_sequence(self, sequence: npt.NDArray) -> npt.NDArray: - ''' - Append a sequence to the end of the circular buffer. - Attempts to append as many elements as possible, returns elements that were not appended. - - Parameters: - sequence (numpy array): Numpy array to append to buffer - - Returns: - Numpy array containing elements that were not appended in the same order as provided. - ''' - len_to_copy = min(self.max_size - self.end, len(sequence)) - self.array[self.end:self.end + len_to_copy] = sequence[:len_to_copy] - self.end += len_to_copy - - return sequence[len_to_copy:] - - def get_curr_buffer(self) -> npt.NDArray: - ''' - Returns: - A numpy view of the current elements in buffer - ''' - return self.array[:self.end] - - def shift_buffer(self, shift: int) -> None: - ''' - Shifts buffer by a given number of elements. - Oldest elements are shifted out first. - - Parameters: - shift (int): Number of elements to shift oit. - ''' - assert isinstance(shift, int), "Shift must be an integer" - assert shift >= 0, "Shift must be nonnegative" - - shift = min(self.end, shift) - self.array = np.roll(self.array, -shift) - self.end -= shift - - def __len__(self) -> int: - ''' - Returns: - Length of current buffer - ''' - return self.end diff --git a/whisper-service/utils/np_circular_buffer_test.py b/whisper-service/utils/np_circular_buffer_test.py deleted file mode 100644 index 89a5211..0000000 --- a/whisper-service/utils/np_circular_buffer_test.py +++ /dev/null @@ -1,176 +0,0 @@ -''' -Unit tests for NPCircularBuffer class -''' -import numpy as np -from utils.np_circular_buffer import NPCircularBuffer - - -def test_single_element_append(): - ''' - Tests appending one elements at a time to buffer - ''' - buffer = NPCircularBuffer(2) - - assert np.array_equal( - buffer.append_sequence(np.array([1])), - np.array([]) - ), "All elements inserted" - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([1]) - ), "Correct sequence in buffer" - - assert np.array_equal( - buffer.append_sequence(np.array([2])), - np.array([]) - ), "All elements inserted" - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([1, 2]) - ), "Correct sequence in buffer" - - assert np.array_equal( - buffer.append_sequence(np.array([3])), - np.array([3]) - ), "All elements inserted" - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([1, 2]) - ), "Correct sequence in buffer" - - -def test_multi_element_append(): - ''' - Tests appending multiple elements at a time to buffer - ''' - buffer = NPCircularBuffer(7) - - assert np.array_equal( - buffer.append_sequence(np.array([1, 2, 3])), - np.array([]) - ), "All elements inserted" - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([1, 2, 3]) - ), "Correct sequence in buffer" - - assert np.array_equal( - buffer.append_sequence(np.array([4, 5, 6])), - np.array([]) - ), "All elements inserted" - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([1, 2, 3, 4, 5, 6]) - ), "Correct sequence in buffer" - - assert np.array_equal( - buffer.append_sequence(np.array([7, 8, 9])), - np.array([8, 9]) - ), "All elements inserted" - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([1, 2, 3, 4, 5, 6, 7]) - ), "Correct sequence in buffer" - - -def test_shift_buffer(): - ''' - Tests shifting out elements from the buffer - ''' - buffer = NPCircularBuffer(10) - - buffer.append_sequence(np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])) - buffer.shift_buffer(1) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([2, 3, 4, 5, 6, 7, 8, 9]) - ), "Correct sequence in buffer" - - buffer.shift_buffer(5) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([7, 8, 9]) - ), "Correct sequence in buffer" - - buffer.shift_buffer(5) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([]) - ), "Correct sequence in buffer" - - -def test_len(): - ''' - Tests that buffer returns correct length - ''' - buffer = NPCircularBuffer(5) - - assert len(buffer) == 0, "Reports correct length" - - buffer.append_sequence(np.array([1, 2])) - assert len(buffer) == 2, "Reports correct length" - - buffer.append_sequence(np.array([3, 4, 5])) - assert len(buffer) == 5, "Reports correct length" - - buffer.append_sequence(np.array([6, 7])) - assert len(buffer) == 5, "Reports correct length" - - buffer.shift_buffer(3) - assert len(buffer) == 2, "Reports correct length" - - -def test_shift_and_append(): - ''' - Test combinations of shifts and appends to buffer - ''' - buffer = NPCircularBuffer(10) - - buffer.append_sequence(np.array([1, 2, 3, 4, 5])) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([1, 2, 3, 4, 5]) - ), "Correct sequence in buffer" - - buffer.shift_buffer(3) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([4, 5]) - ), "Correct sequence in buffer" - - buffer.append_sequence(np.array([6, 7, 8, 9, 10, 11, 12])) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([4, 5, 6, 7, 8, 9, 10, 11, 12]) - ), "Correct sequence in buffer" - - buffer.shift_buffer(5) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([9, 10, 11, 12]) - ), "Correct sequence in buffer" - - buffer.append_sequence(np.array([13, 14, 15, 16, 17, 18, 19, 20])) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([9, 10, 11, 12, 13, 14, 15, 16, 17, 18]) - ), "Correct sequence in buffer" - - buffer.shift_buffer(15) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([]) - ), "Correct sequence in buffer" - - buffer.append_sequence( - np.array([21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32])) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([21, 22, 23, 24, 25, 26, 27, 28, 29, 30]) - ), "Correct sequence in buffer" - - buffer.shift_buffer(10) - assert np.array_equal( - buffer.get_curr_buffer(), - np.array([]) - ), "Correct sequence in buffer"