diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..77378eaedb679c8c394555614a522e38852d2983 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm + +# Install MongoDB tools (mongosh, mongorestore, mongodump) directly from MongoDB repository +RUN curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg && \ + echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list && \ + apt-get update && \ + apt-get install -y mongodb-mongosh mongodb-database-tools vim && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000000000000000000000000000000000..895b06c884756d79250277c3249f22e7d48946cc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + "dockerfile": "Dockerfile" + }, + + "customizations": { + "vscode": { + "extensions": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "svelte.svelte-vscode"] + } + }, + + "features": { + // Install docker in container + "ghcr.io/devcontainers/features/docker-in-docker:2": { + // Use proprietary docker engine. I get a timeout error when using the default moby engine and loading + // microsoft's PGP keys + "moby": false + } + } + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..87af36b131347f2159ad9cc111ee4eb36907d6ab --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +Dockerfile +.vscode/ +.idea +.gitignore +LICENSE +README.md +node_modules/ +.svelte-kit/ +.env* +!.env +.env.local +db +models/** \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..66154f545b8838bbb39f1ad9580c12671b49c937 --- /dev/null +++ b/.env @@ -0,0 +1,170 @@ +# Use .env.local to change these variables +# DO NOT EDIT THIS FILE WITH SENSITIVE DATA + +### Models ### +# Models are sourced exclusively from an OpenAI-compatible base URL. +# Example: https://router.huggingface.co/v1 +OPENAI_BASE_URL=https://router.huggingface.co/v1 + +# Canonical auth token for any OpenAI-compatible provider +OPENAI_API_KEY=#your provider API key (works for HF router, OpenAI, LM Studio, etc.). +# When set to true, user token will be used for inference calls +USE_USER_TOKEN=false +# Automatically redirect to oauth login page if user is not logged in, when set to "true" +AUTOMATIC_LOGIN=false + +### MongoDB ### +MONGODB_URL=#your mongodb URL here, use chat-ui-db image if you don't want to set this +MONGODB_DB_NAME=chat-ui +MONGODB_DIRECT_CONNECTION=false + + +## Public app configuration ## +PUBLIC_APP_NAME=ChatUI # name used as title throughout the app +PUBLIC_APP_ASSETS=chatui # used to find logos & favicons in static/$PUBLIC_APP_ASSETS +PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone."# description used throughout the app +PUBLIC_SMOOTH_UPDATES=false # set to true to enable smoothing of messages client-side, can be CPU intensive +PUBLIC_ORIGIN= +PUBLIC_SHARE_PREFIX= +PUBLIC_GOOGLE_ANALYTICS_ID= +PUBLIC_PLAUSIBLE_SCRIPT_URL= +PUBLIC_APPLE_APP_ID= + +COUPLE_SESSION_WITH_COOKIE_NAME= +# when OPEN_ID is configured, users are required to login after the welcome modal +OPENID_CLIENT_ID="" # You can set to "__CIMD__" for automatic oauth app creation when deployed +OPENID_CLIENT_SECRET= +OPENID_SCOPES="openid profile inference-api" +USE_USER_TOKEN= +AUTOMATIC_LOGIN=# if true authentication is required on all routes + +### Local Storage ### +MONGO_STORAGE_PATH= # where is the db folder stored + +## Models overrides +MODELS= + +## Task model +# Optional: set to the model id/name from the `${OPENAI_BASE_URL}/models` list +# to use for internal tasks (title summarization, etc). If not set, the current model will be used +TASK_MODEL= + +# Arch router (OpenAI-compatible) endpoint base URL used for route selection +# Example: https://api.openai.com/v1 or your hosted Arch endpoint +LLM_ROUTER_ARCH_BASE_URL= + +## LLM Router Configuration +# Path to routes policy (JSON array). Defaults to llm-router/routes.chat.json +LLM_ROUTER_ROUTES_PATH= + +# Model used at the Arch router endpoint for selection +LLM_ROUTER_ARCH_MODEL= + +# Fallback behavior +# Route to map "other" to (must exist in routes file) +LLM_ROUTER_OTHER_ROUTE=casual_conversation +# Model to call if the Arch selection fails entirely +LLM_ROUTER_FALLBACK_MODEL= +# Arch selection timeout in milliseconds (default 10000) +LLM_ROUTER_ARCH_TIMEOUT_MS=10000 +# Maximum length (in characters) for assistant messages sent to router for route selection (default 500) +LLM_ROUTER_MAX_ASSISTANT_LENGTH=500 +# Maximum length (in characters) for previous user messages sent to router (latest user message not trimmed, default 400) +LLM_ROUTER_MAX_PREV_USER_LENGTH=400 + +# Enable router multimodal fallback (set to true to allow image inputs via router) +LLM_ROUTER_ENABLE_MULTIMODAL=false +# Optional: specific model to use for multimodal requests. If not set, uses first multimodal model +LLM_ROUTER_MULTIMODAL_MODEL= + +# Router UI overrides (client-visible) +# Public display name for the router entry in the model list. Defaults to "Omni". +PUBLIC_LLM_ROUTER_DISPLAY_NAME=Omni +# Optional: public logo URL for the router entry. If unset, the UI shows a Carbon icon. +PUBLIC_LLM_ROUTER_LOGO_URL= +# Public alias id used for the virtual router model (Omni). Defaults to "omni". +PUBLIC_LLM_ROUTER_ALIAS_ID=omni + +### Authentication ### +# Parameters to enable open id login +OPENID_CONFIG= +# if it's defined, only these emails will be allowed to use login +ALLOWED_USER_EMAILS=[] +# If it's defined, users with emails matching these domains will also be allowed to use login +ALLOWED_USER_DOMAINS=[] +# valid alternative redirect URLs for OAuth, used for HuggingChat apps +ALTERNATIVE_REDIRECT_URLS=[] +### Cookies +# name of the cookie used to store the session +COOKIE_NAME=hf-chat +# If the value of this cookie changes, the session is destroyed. Useful if chat-ui is deployed on a subpath +# of your domain, and you want chat ui sessions to reset if the user's auth changes +COUPLE_SESSION_WITH_COOKIE_NAME= +# specify secure behaviour for cookies +COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty +COOKIE_SECURE=# set to true to only allow cookies over https +TRUSTED_EMAIL_HEADER=# header to use to get the user email, only use if you know what you are doing + +### Admin stuff ### +ADMIN_CLI_LOGIN=true # set to false to disable the CLI login +ADMIN_TOKEN=#We recommend leaving this empty, you can get the token from the terminal. + +### Feature Flags ### +LLM_SUMMARIZATION=true # generate conversation titles with LLMs + +ALLOW_IFRAME=true # Allow the app to be embedded in an iframe +ENABLE_DATA_EXPORT=true + +### Rate limits ### +# See `src/lib/server/usageLimits.ts` +# { +# conversations: number, # how many conversations +# messages: number, # how many messages in a conversation +# assistants: number, # how many assistants +# messageLength: number, # how long can a message be before we cut it off +# messagesPerMinute: number, # how many messages per minute +# tools: number # how many tools +# } +USAGE_LIMITS={} + +### HuggingFace specific ### +## Feature flag & admin settings +# Used for setting early access & admin flags to users +HF_ORG_ADMIN= +HF_ORG_EARLY_ACCESS= +WEBHOOK_URL_REPORT_ASSISTANT=#provide slack webhook url to get notified for reports/feature requests + + +### Metrics ### +METRICS_ENABLED=false +METRICS_PORT=5565 +LOG_LEVEL=info + + +### Parquet export ### +# Not in use anymore but useful to export conversations to a parquet file as a HuggingFace dataset +PARQUET_EXPORT_DATASET= +PARQUET_EXPORT_HF_TOKEN= +ADMIN_API_SECRET=# secret to admin API calls, like computing usage stats or exporting parquet data + +### Config ### +ENABLE_CONFIG_MANAGER=true + +### Docker build variables ### +# These values cannot be updated at runtime +# They need to be passed when building the docker image +# See https://github.com/huggingface/chat-ui/main/.github/workflows/deploy-prod.yml#L44-L47 +APP_BASE="" # base path of the app, e.g. /chat, left blank as default +### Body size limit for SvelteKit https://svelte.dev/docs/kit/adapter-node#Environment-variables-BODY_SIZE_LIMIT +BODY_SIZE_LIMIT=15728640 +PUBLIC_COMMIT_SHA= + +### LEGACY parameters +ALLOW_INSECURE_COOKIES=false # LEGACY! Use COOKIE_SECURE and COOKIE_SAMESITE instead +PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead +RATE_LIMIT= # /!\ DEPRECATED definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead +OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not provide name +OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com +OPENID_TOLERANCE= +OPENID_RESOURCE= +EXPOSE_API=# deprecated, API is now always exposed diff --git a/.env.ci b/.env.ci new file mode 100644 index 0000000000000000000000000000000000000000..2e0dab4af7f17dc1e632689e30bcc5f45a1f0db7 --- /dev/null +++ b/.env.ci @@ -0,0 +1 @@ +MONGODB_URL=mongodb://localhost:27017/ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..38972655faff07d2cc0383044bbf9f43b22c2248 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000000000000000000000000000000000..9c0da75f9cc3b351d432d8d0bd706c42cb576f7e --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,45 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:svelte/recommended", + "prettier", + ], + plugins: ["@typescript-eslint"], + ignorePatterns: ["*.cjs"], + overrides: [ + { + files: ["*.svelte"], + parser: "svelte-eslint-parser", + parserOptions: { + parser: "@typescript-eslint/parser", + }, + }, + ], + parserOptions: { + sourceType: "module", + ecmaVersion: 2020, + extraFileExtensions: [".svelte"], + }, + rules: { + "no-empty": "off", + "require-yield": "off", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-unused-vars": [ + // prevent variables with a _ prefix from being marked as unused + "error", + { + argsIgnorePattern: "^_", + }, + ], + "object-shorthand": ["error", "always"], + }, + env: { + browser: true, + es2017: true, + node: true, + }, +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..a4ac6de97dc949e28ad328a035a0a4f26e793cf8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*/.ttf filter=lfs diff=lfs merge=lfs -text +static/huggingchat/tools-thumbnail.png filter=lfs diff=lfs merge=lfs -text +static/huggingchat/assistants-thumbnail.png filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md b/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md new file mode 100644 index 0000000000000000000000000000000000000000..22a7664a9c01e122c116af38a18fef1ff0c2b7a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md @@ -0,0 +1,43 @@ +--- +name: Bug Report (chat-ui) +about: Use this for confirmed issues with chat-ui +title: "" +labels: bug +assignees: "" +--- + +## Bug description + + + +## Steps to reproduce + + + +## Screenshots + + + +## Context + +### Logs + + + +``` +// logs here if relevant +``` + +### Specs + +- **OS**: +- **Browser**: +- **chat-ui commit**: + +### Config + + + +## Notes + + diff --git a/.github/ISSUE_TEMPLATE/config-support.md b/.github/ISSUE_TEMPLATE/config-support.md new file mode 100644 index 0000000000000000000000000000000000000000..bd858036f15992ec51ca924243b4bbf6363f597e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config-support.md @@ -0,0 +1,9 @@ +--- +name: Config Support +about: Help with setting up chat-ui locally +title: "" +labels: support +assignees: "" +--- + +**Please use the discussions on GitHub** for getting help with setting things up instead of opening an issue: https://github.com/huggingface/chat-ui/discussions diff --git a/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md b/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md new file mode 100644 index 0000000000000000000000000000000000000000..cc9adf91f0f938a12510ecaa104947e198f36196 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md @@ -0,0 +1,17 @@ +--- +name: Feature Request (chat-ui) +about: Suggest new features to be added to chat-ui +title: "" +labels: enhancement +assignees: "" +--- + +## Describe your feature request + + + +## Screenshots (if relevant) + +## Implementation idea + + diff --git a/.github/ISSUE_TEMPLATE/huggingchat.md b/.github/ISSUE_TEMPLATE/huggingchat.md new file mode 100644 index 0000000000000000000000000000000000000000..0716f9baaefb9b69ffaafc1b67f522c5b8753111 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/huggingchat.md @@ -0,0 +1,11 @@ +--- +name: HuggingChat +about: Requests & reporting outages on HuggingChat, the hosted version of chat-ui. +title: "" +labels: huggingchat +assignees: "" +--- + +**Do not use GitHub issues** for requesting models on HuggingChat or reporting issues with HuggingChat being down/overloaded. + +**Use the discussions page on the hub instead:** https://huggingface.co/spaces/huggingchat/chat-ui/discussions diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000000000000000000000000000000000..3a183679f1aa435bf266e800f343d91ef355eabd --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,16 @@ +changelog: + exclude: + labels: + - huggingchat + - CI/CD + - documentation + categories: + - title: Features + labels: + - enhancement + - title: Bugfixes + labels: + - bug + - title: Other changes + labels: + - "*" diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd6109421f3d6f160c174d894f1cc9281cb0903c --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,18 @@ +name: Build documentation + +on: + push: + branches: + - main + - v*-release + +jobs: + build: + uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@main + with: + commit_sha: ${{ github.sha }} + package: chat-ui + additional_args: --not_python_module + secrets: + token: ${{ secrets.HUGGINGFACE_PUSH }} + hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000000000000000000000000000000000000..87e411f622e40b4642a78bbaee833079eaa60f5b --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,142 @@ +name: Build and Publish Image + +permissions: + packages: write + +on: + push: + branches: + - "main" + pull_request: + branches: + - "*" + paths: + - "Dockerfile" + - "entrypoint.sh" + workflow_dispatch: + release: + types: [published, edited] + +jobs: + build-and-publish-image-with-db: + runs-on: + group: aws-general-8-plus + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract package version + id: package-version + run: | + VERSION=$(jq -r .version package.json) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + MAJOR=$(echo $VERSION | cut -d '.' -f1) + echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT + MINOR=$(echo $VERSION | cut -d '.' -f1).$(echo $VERSION | cut -d '.' -f2) + echo "MINOR=$MINOR" >> $GITHUB_OUTPUT + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/huggingface/chat-ui-db + tags: | + type=raw,value=${{ steps.package-version.outputs.VERSION }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MAJOR }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MINOR }},enable=${{github.event_name == 'release'}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha,enable={{is_default_branch}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Build and Publish Docker Image with DB + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + INCLUDE_DB=true + PUBLIC_COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }} + build-and-publish-image-nodb: + runs-on: + group: aws-general-8-plus + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract package version + id: package-version + run: | + VERSION=$(jq -r .version package.json) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + MAJOR=$(echo $VERSION | cut -d '.' -f1) + echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT + MINOR=$(echo $VERSION | cut -d '.' -f1).$(echo $VERSION | cut -d '.' -f2) + echo "MINOR=$MINOR" >> $GITHUB_OUTPUT + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/huggingface/chat-ui + tags: | + type=raw,value=${{ steps.package-version.outputs.VERSION }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MAJOR }},enable=${{github.event_name == 'release'}} + type=raw,value=${{ steps.package-version.outputs.MINOR }},enable=${{github.event_name == 'release'}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha,enable={{is_default_branch}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Build and Publish Docker Image without DB + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + INCLUDE_DB=false + PUBLIC_COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }} diff --git a/.github/workflows/build-pr-docs.yml b/.github/workflows/build-pr-docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..9216112730ec29d76465667c80816e131d2c6755 --- /dev/null +++ b/.github/workflows/build-pr-docs.yml @@ -0,0 +1,20 @@ +name: Build PR Documentation + +on: + pull_request: + paths: + - "docs/source/**" + - ".github/workflows/build-pr-docs.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main + with: + commit_sha: ${{ github.event.pull_request.head.sha }} + pr_number: ${{ github.event.number }} + package: chat-ui + additional_args: --not_python_module diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..ef07064ef28c022b3d0baa545f1ac8773fa7b467 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,62 @@ +name: Deploy to ephemeral +on: + pull_request: + +jobs: + branch-slug: + uses: ./.github/workflows/slugify.yaml + with: + value: ${{ github.head_ref }} + + deploy-dev: + if: contains(github.event.pull_request.labels.*.name, 'preview') + runs-on: ubuntu-latest + needs: branch-slug + environment: + name: dev + url: https://${{ needs.branch-slug.outputs.slug }}.chat-dev.huggingface.tech/chat/ + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Set GITHUB_SHA_SHORT from PR + if: env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT != null + run: echo "GITHUB_SHA_SHORT=${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}" >> $GITHUB_ENV + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + huggingface/chat-ui + tags: | + type=raw,value=dev-${{ env.GITHUB_SHA_SHORT }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and Publish HuggingChat image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + cache-to: type=gha,mode=max,scope=amd64 + cache-from: type=gha,scope=amd64 + provenance: false + build-args: | + INCLUDE_DB=false + APP_BASE=/chat + PUBLIC_COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }} diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000000000000000000000000000000000000..dc0a4d12630ea9195e14ce54365cb443cc7e68fe --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,78 @@ +name: Deploy to k8s +on: + # run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build-and-publish-huggingchat-image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + huggingface/chat-ui + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,enable=true,prefix=sha-,format=short,sha-len=8 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Build and Publish HuggingChat image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + cache-to: type=gha,mode=max,scope=amd64 + cache-from: type=gha,scope=amd64 + provenance: false + build-args: | + INCLUDE_DB=false + APP_BASE=/chat + PUBLIC_COMMIT_SHA=${{ env.GITHUB_SHA_SHORT }} + deploy: + name: Deploy on prod + runs-on: ubuntu-latest + needs: ["build-and-publish-huggingchat-image"] + steps: + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@v4.5.0 + + - name: Gen values + run: | + VALUES=$(cat <<-END + image: + tag: "sha-${{ env.GITHUB_SHA_SHORT }}" + END + ) + echo "VALUES=$(echo "$VALUES" | yq -o=json | jq tostring)" >> $GITHUB_ENV + + - name: Deploy on infra-deployments + uses: aurelien-baudet/workflow-dispatch@v2 + with: + workflow: Update application single value + repo: huggingface/infra-deployments + wait-for-completion: true + wait-for-completion-interval: 10s + display-workflow-run-url-interval: 10s + ref: refs/heads/main + token: ${{ secrets.GIT_TOKEN_INFRA_DEPLOYMENT }} + inputs: '{"path": "hub/chat-ui/chat-ui.yaml", "value": ${{ env.VALUES }}, "url": "${{ github.event.head_commit.url }}"}' diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..1c3f3708d39fef9cb25d539f676991202e8a1042 --- /dev/null +++ b/.github/workflows/lint-and-test.yml @@ -0,0 +1,84 @@ +name: Lint and test + +on: + pull_request: + push: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + - run: | + npm install ci + - name: "Checking lint/format errors" + run: | + npm run lint + - name: "Checking type errors" + run: | + npm run check + + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + - run: | + npm ci + npx playwright install + - name: "Tests" + run: | + npm run test + + build-check: + runs-on: + group: aws-general-8-plus + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + - name: Build Docker image + run: | + docker build \ + --build-arg INCLUDE_DB=true \ + -t chat-ui-test:latest . + + - name: Run Docker container + run: | + export DOTENV_LOCAL=$(<.env.ci) + docker run -d --rm --network=host \ + --name chat-ui-test \ + -e DOTENV_LOCAL="$DOTENV_LOCAL" \ + chat-ui-test:latest + + - name: Wait for server to start + run: | + for i in {1..10}; do + if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/ | grep -q "200"; then + echo "Server is up" + exit 0 + fi + echo "Waiting for server..." + sleep 2 + done + echo "Server did not start in time" + docker logs chat-ui-test + exit 1 + + - name: Stop Docker container + if: always() + run: | + docker stop chat-ui-test || true diff --git a/.github/workflows/slugify.yaml b/.github/workflows/slugify.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8f63c34e437d12ec4c58df79911a7a655ba9eece --- /dev/null +++ b/.github/workflows/slugify.yaml @@ -0,0 +1,72 @@ +name: Generate Branch Slug + +on: + workflow_call: + inputs: + value: + description: 'Value to slugify' + required: true + type: string + outputs: + slug: + description: 'Slugified value' + value: ${{ jobs.generate-slug.outputs.slug }} + +jobs: + generate-slug: + runs-on: ubuntu-latest + outputs: + slug: ${{ steps.slugify.outputs.slug }} + + steps: + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Generate slug + id: slugify + run: | + # Create working directory + mkdir -p $HOME/slugify + cd $HOME/slugify + + # Create Go script + cat > main.go << 'EOF' + package main + + import ( + "fmt" + "os" + "github.com/gosimple/slug" + ) + + func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: slugify ") + os.Exit(1) + } + + text := os.Args[1] + slugged := slug.Make(text) + fmt.Println(slugged) + } + EOF + + # Initialize module and install dependency + go mod init slugify + go mod tidy + go get github.com/gosimple/slug + + # Build + go build -o slugify main.go + + # Generate slug + VALUE="${{ inputs.value }}" + echo "Input value: $VALUE" + + SLUG=$(./slugify "$VALUE") + echo "Generated slug: $SLUG" + + # Export + echo "slug=$SLUG" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml new file mode 100644 index 0000000000000000000000000000000000000000..bd49d7cc03d279637692f699308398a09388b7be --- /dev/null +++ b/.github/workflows/trufflehog.yml @@ -0,0 +1,17 @@ +on: + push: + +name: Secret Leaks + +jobs: + trufflehog: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Secret Scanning + uses: trufflesecurity/trufflehog@main + with: + extra_args: --results=verified,unknown diff --git a/.github/workflows/upload-pr-documentation.yml b/.github/workflows/upload-pr-documentation.yml new file mode 100644 index 0000000000000000000000000000000000000000..091d9423e04d18e8afc55836edec9c16556e32c0 --- /dev/null +++ b/.github/workflows/upload-pr-documentation.yml @@ -0,0 +1,16 @@ +name: Upload PR Documentation + +on: + workflow_run: + workflows: ["Build PR Documentation"] + types: + - completed + +jobs: + build: + uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@main + with: + package_name: chat-ui + secrets: + hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} + comment_bot_token: ${{ secrets.COMMENT_BOT_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..abc7a800c6bb20e52cd95dbccd2ad3617a74cc94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +SECRET_CONFIG +.idea +!.env.ci +!.env +gcp-*.json +db +models/* +!models/add-your-models-here.txt \ No newline at end of file diff --git a/.husky/lint-stage-config.js b/.husky/lint-stage-config.js new file mode 100644 index 0000000000000000000000000000000000000000..abab8885bcc6f9aa09b83f762c77b75ed8fd3e8b --- /dev/null +++ b/.husky/lint-stage-config.js @@ -0,0 +1,4 @@ +export default { + "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix", "eslint"], + "*.json": ["prettier --write"], +}; diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000000000000000000000000000000000..4d9467a4abbaa51e20bfb0238b7df3c466a0cb91 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +set -e +npx lint-staged --config ./.husky/lint-stage-config.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000000000000000000000000000000..b6f27f135954640c8cc5bfd7b8c9922ca6eb2aad --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000000000000000000000000000000..177a4e072adf23e4836601b8d113f6adf1c411a8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +/chart +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..de36577e274217d824839b69b08e04a130509732 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "useTabs": true, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..52a466a5b80955092c9df3a0f128003683757d5f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "npm run dev", + "name": "Run development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..670fc9d919e3ddbce1ee622afc9d6fb6977d4fd3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "eslint.validate": ["javascript", "svelte"], + "[svelte]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ebe6286d7fee3a41cc3bc48e76ed8e712bb07709 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,93 @@ +# syntax=docker/dockerfile:1 +ARG INCLUDE_DB=false + +FROM node:24-slim AS base + +# install dotenv-cli +RUN npm install -g dotenv-cli + +# switch to a user that works for spaces +RUN userdel -r node +RUN useradd -m -u 1000 user +USER user + +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH + +WORKDIR /app + +# add a .env.local if the user doesn't bind a volume to it +RUN touch /app/.env.local + +USER root +RUN apt-get update +RUN apt-get install -y libgomp1 libcurl4 curl dnsutils nano + +# ensure npm cache dir exists before adjusting ownership +RUN mkdir -p /home/user/.npm && chown -R 1000:1000 /home/user/.npm + +USER user + + +COPY --chown=1000 .env /app/.env +COPY --chown=1000 entrypoint.sh /app/entrypoint.sh +COPY --chown=1000 package.json /app/package.json +COPY --chown=1000 package-lock.json /app/package-lock.json + +RUN chmod +x /app/entrypoint.sh + +FROM node:24 AS builder + +WORKDIR /app + +COPY --link --chown=1000 package-lock.json package.json ./ + +ARG APP_BASE= +ARG PUBLIC_APP_COLOR= +ENV BODY_SIZE_LIMIT=15728640 + +RUN --mount=type=cache,target=/app/.npm \ + npm set cache /app/.npm && \ + npm ci + +COPY --link --chown=1000 . . + +RUN git config --global --add safe.directory /app && \ + npm run build + +# mongo image +FROM mongo:7 AS mongo + +# image to be used if INCLUDE_DB is false +FROM base AS local_db_false + +# image to be used if INCLUDE_DB is true +FROM base AS local_db_true + +# copy mongo from the other stage +COPY --from=mongo /usr/bin/mongo* /usr/bin/ + +ENV MONGODB_URL=mongodb://localhost:27017 +USER root +RUN mkdir -p /data/db +RUN chown -R 1000:1000 /data/db +USER user +# final image +FROM local_db_${INCLUDE_DB} AS final + +# build arg to determine if the database should be included +ARG INCLUDE_DB=false +ENV INCLUDE_DB=${INCLUDE_DB} + +# svelte requires APP_BASE at build time so it must be passed as a build arg +ARG APP_BASE= +ARG PUBLIC_APP_COLOR= +ARG PUBLIC_COMMIT_SHA= +ENV PUBLIC_COMMIT_SHA=${PUBLIC_COMMIT_SHA} +ENV BODY_SIZE_LIMIT=15728640 + +#import the build & dependencies +COPY --from=builder --chown=1000 /app/build /app/build +COPY --from=builder --chown=1000 /app/node_modules /app/node_modules + +CMD ["/bin/bash", "-c", "/app/entrypoint.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e44d8f5b79a0643c99977835611e1da9d08fc3cf --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ +Copyright 2018- The Hugging Face team. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000000000000000000000000000000000000..fc3bbfc8270bd7ffd6c60a2f74cc3773e684ddec --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,41 @@ +## Privacy + +> Last updated: Sep 15, 2025 + +Basics: + +- Sign-in: You authenticate with your Hugging Face account. +- Conversation history: Stored so you can access past chats; you can delete any conversation at any time from the UI. + +🗓 Please also consult huggingface.co's main privacy policy at . To exercise any of your legal privacy rights, please send an email to . + +## Data handling and processing + +HuggingChat uses Hugging Face’s Inference Providers to access models from multiple partners via a single API. Depending on the model and availability, inference runs with the corresponding provider. + +- Inference Providers documentation: +- Security & Compliance: + +Security and routing facts + +- Hugging Face does not store any user data for training purposes. +- Hugging Face does not store the request body or the response when routing requests through Hugging Face. +- Logs are kept for debugging purposes for up to 30 days, but no user data or tokens are stored in those logs. +- Inference Provider routing uses TLS/SSL to encrypt data in transit. +- The Hugging Face Hub (which Inference Providers is a feature of) is SOC 2 Type 2 certified. See . + +External providers are responsible for their own security and data handling. Please consult each provider’s respective security and privacy policies via the Inference Providers documentation linked above. + +## Technical details + +[![chat-ui](https://img.shields.io/github/stars/huggingface/chat-ui)](https://github.com/huggingface/chat-ui) + +The app is completely open source, and further development takes place on the [huggingface/chat-ui](https://github.com/huggingface/chat-ui) GitHub repo. We're always open to contributions! + +You can find the production configuration for HuggingChat [here](https://github.com/huggingface/chat-ui/blob/main/chart/env/prod.yaml). + +HuggingChat connects to the OpenAI‑compatible Inference Providers router at `https://router.huggingface.co/v1` to access models across multiple providers. Provider selection may be automatic or fixed depending on the model configuration. + +We welcome any feedback on this app: please participate in the public discussion at + + diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..56a4f882f055d9ae66ff6334e267bff781f7ac68 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +--- +title: Chat Ui +emoji: 🐠 +colorFrom: pink +colorTo: gray +sdk: docker +pinned: false +app_port: 3000 +--- + +# Chat UI + +![Chat UI repository thumbnail](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/chat-ui/chat-ui-2026.png) + +A chat interface for LLMs. It is a SvelteKit app and it powers the [HuggingChat app on hf.co/chat](https://huggingface.co/chat). + +0. [Quickstart](#quickstart) +1. [Database Options](#database-options) +2. [Launch](#launch) +3. [Optional Docker Image](#optional-docker-image) +4. [Extra parameters](#extra-parameters) +5. [Building](#building) + +> [!NOTE] +> Chat UI only supports OpenAI-compatible APIs via `OPENAI_BASE_URL` and the `/models` endpoint. Provider-specific integrations (legacy `MODELS` env var, GGUF discovery, embeddings, web-search helpers, etc.) are removed, but any service that speaks the OpenAI protocol (llama.cpp server, Ollama, OpenRouter, etc. will work by default). + +> [!NOTE] +> The old version is still available on the [legacy branch](https://github.com/huggingface/chat-ui/tree/legacy) + +## Quickstart + +Chat UI speaks to OpenAI-compatible APIs only. The fastest way to get running is with the Hugging Face Inference Providers router plus your personal Hugging Face access token. + +**Step 1 – Create `.env.local`:** + +```env +OPENAI_BASE_URL=https://router.huggingface.co/v1 +OPENAI_API_KEY=hf_************************ +# Fill in once you pick a database option below +MONGODB_URL= +``` + +`OPENAI_API_KEY` can come from any OpenAI-compatible endpoint you plan to call. Pick the combo that matches your setup and drop the values into `.env.local`: + +| Provider | Example `OPENAI_BASE_URL` | Example key env | +| --------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------- | +| Hugging Face Inference Providers router | `https://router.huggingface.co/v1` | `OPENAI_API_KEY=hf_xxx` (or `HF_TOKEN` legacy alias) | +| llama.cpp server (`llama.cpp --server --api`) | `http://127.0.0.1:8080/v1` | `OPENAI_API_KEY=sk-local-demo` (any string works; llama.cpp ignores it) | +| Ollama (with OpenAI-compatible bridge) | `http://127.0.0.1:11434/v1` | `OPENAI_API_KEY=ollama` | +| OpenRouter | `https://openrouter.ai/api/v1` | `OPENAI_API_KEY=sk-or-v1-...` | +| Poe | `https://api.poe.com/v1` | `OPENAI_API_KEY=pk_...` | + +Check the root [`.env` template](./.env) for the full list of optional variables you can override. + +**Step 2 – Choose where MongoDB lives:** Either provision a managed cluster (for example MongoDB Atlas) or run a local container. Both approaches are described in [Database Options](#database-options). After you have the URI, drop it into `MONGODB_URL` (and, if desired, set `MONGODB_DB_NAME`). + +**Step 3 – Install and launch the dev server:** + +```bash +git clone https://github.com/huggingface/chat-ui +cd chat-ui +npm install +npm run dev -- --open +``` + +You now have Chat UI running against the Hugging Face router without needing to host MongoDB yourself. + +## Database Options + +Chat history, users, settings, files, and stats all live in MongoDB. You can point Chat UI at any MongoDB 6/7 deployment. + +### MongoDB Atlas (managed) + +1. Create a free cluster at [mongodb.com](https://www.mongodb.com/pricing). +2. Add your IP (or `0.0.0.0/0` for development) to the network access list. +3. Create a database user and copy the connection string. +4. Paste that string into `MONGODB_URL` in `.env.local`. Keep the default `MONGODB_DB_NAME=chat-ui` or change it per environment. + +Atlas keeps MongoDB off your laptop, which is ideal for teams or cloud deployments. + +### Local MongoDB (container) + +If you prefer to run MongoDB locally: + +```bash +docker run -d -p 27017:27017 --name mongo-chatui mongo:latest +``` + +Then set `MONGODB_URL=mongodb://localhost:27017` in `.env.local`. You can also supply `MONGO_STORAGE_PATH` if you want Chat UI’s fallback in-memory server to persist under a specific folder. + +## Launch + +After configuring your environment variables, start Chat UI with: + +```bash +npm install +npm run dev +``` + +The dev server listens on `http://localhost:5173` by default. Use `npm run build` / `npm run preview` for production builds. + +## Optional Docker Image + +Prefer containerized setup? You can run everything in one container as long as you supply a MongoDB URI (local or hosted): + +```bash +docker run \ + -p 3000 \ + -e MONGODB_URL=mongodb://host.docker.internal:27017 \ + -e OPENAI_BASE_URL=https://router.huggingface.co/v1 \ + -e OPENAI_API_KEY=hf_*** \ + -v db:/data \ + ghcr.io/huggingface/chat-ui-db:latest +``` + +`host.docker.internal` lets the container reach a MongoDB instance on your host machine; swap it for your Atlas URI if you use the hosted option. All environment variables accepted in `.env.local` can be provided as `-e` flags. + +## Extra parameters + +### Theming + +You can use a few environment variables to customize the look and feel of chat-ui. These are by default: + +```env +PUBLIC_APP_NAME=ChatUI +PUBLIC_APP_ASSETS=chatui +PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone." +PUBLIC_APP_DATA_SHARING= +``` + +- `PUBLIC_APP_NAME` The name used as a title throughout the app. +- `PUBLIC_APP_ASSETS` Is used to find logos & favicons in `static/$PUBLIC_APP_ASSETS`, current options are `chatui` and `huggingchat`. +- `PUBLIC_APP_DATA_SHARING` Can be set to 1 to add a toggle in the user settings that lets your users opt-in to data sharing with models creator. + +### Models + +This build does not use the `MODELS` env var or GGUF discovery. Configure models via `OPENAI_BASE_URL` only; Chat UI will fetch `${OPENAI_BASE_URL}/models` and populate the list automatically. Authorization uses `OPENAI_API_KEY` (preferred). `HF_TOKEN` remains a legacy alias. + +### LLM Router (Optional) + +Chat UI can perform client-side routing [katanemo/Arch-Router-1.5B](https://huggingface.co/katanemo/Arch-Router-1.5B) as the routing model without running a separate router service. The UI exposes a virtual model alias called "Omni" (configurable) that, when selected, chooses the best route/model for each message. + +- Provide a routes policy JSON via `LLM_ROUTER_ROUTES_PATH`. No sample file ships with this branch, so you must point the variable to a JSON array you create yourself (for example, commit one in your project like `config/routes.chat.json`). Each route entry needs `name`, `description`, `primary_model`, and optional `fallback_models`. +- Configure the Arch router selection endpoint with `LLM_ROUTER_ARCH_BASE_URL` (OpenAI-compatible `/chat/completions`) and `LLM_ROUTER_ARCH_MODEL` (e.g. `router/omni`). The Arch call reuses `OPENAI_API_KEY` for auth. +- Map `other` to a concrete route via `LLM_ROUTER_OTHER_ROUTE` (default: `casual_conversation`). If Arch selection fails, calls fall back to `LLM_ROUTER_FALLBACK_MODEL`. +- Selection timeout can be tuned via `LLM_ROUTER_ARCH_TIMEOUT_MS` (default 10000). +- Omni alias configuration: `PUBLIC_LLM_ROUTER_ALIAS_ID` (default `omni`), `PUBLIC_LLM_ROUTER_DISPLAY_NAME` (default `Omni`), and optional `PUBLIC_LLM_ROUTER_LOGO_URL`. + +When you select Omni in the UI, Chat UI will: + +- Call the Arch endpoint once (non-streaming) to pick the best route for the last turns. +- Emit RouterMetadata immediately (route and actual model used) so the UI can display it. +- Stream from the selected model via your configured `OPENAI_BASE_URL`. On errors, it tries route fallbacks. + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..477bcc088c304e4430f02b91670c914bf82dd0d8 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: chat-ui +version: 0.0.1-latest +type: application +icon: https://huggingface.co/front/assets/huggingface_logo-noborder.svg diff --git a/chart/env/dev.yaml b/chart/env/dev.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f3a02c79954d1d48964d0ece95dae8c3f1149042 --- /dev/null +++ b/chart/env/dev.yaml @@ -0,0 +1,205 @@ +image: + repository: huggingface + name: chat-ui + +#nodeSelector: +# role-huggingchat: "true" +# +#tolerations: +# - key: "huggingface.co/huggingchat" +# operator: "Equal" +# value: "true" +# effect: "NoSchedule" + +serviceAccount: + enabled: true + create: true + name: huggingchat-ephemeral + +ingress: + enabled: false + +ingressInternal: + enabled: true + path: "/chat" + annotations: + external-dns.alpha.kubernetes.io/hostname: "*.chat-dev.huggingface.tech" + alb.ingress.kubernetes.io/healthcheck-path: "/chat/healthcheck" + alb.ingress.kubernetes.io/listen-ports: "[{\"HTTP\": 80}, {\"HTTPS\": 443}]" + alb.ingress.kubernetes.io/group.name: "chat-dev-internal-public" + alb.ingress.kubernetes.io/load-balancer-name: "chat-dev-internal-public" + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/tags: "Env=prod,Project=hub,Terraform=true" + alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30 + alb.ingress.kubernetes.io/target-type: "ip" + alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-1:707930574880:certificate/bc3eb446-1c04-432c-ac6b-946a88d725da" + kubernetes.io/ingress.class: "alb" + +envVars: + TEST: "test" + COUPLE_SESSION_WITH_COOKIE_NAME: "token" + OPENID_SCOPES: "openid profile inference-api" + USE_USER_TOKEN: "true" + AUTOMATIC_LOGIN: "false" + + ADDRESS_HEADER: "X-Forwarded-For" + APP_BASE: "/chat" + ALLOW_IFRAME: "false" + COOKIE_SAMESITE: "lax" + COOKIE_SECURE: "true" + EXPOSE_API: "true" + METRICS_ENABLED: "true" + LOG_LEVEL: "debug" + NODE_LOG_STRUCTURED_DATA: "true" + + OPENAI_BASE_URL: "https://router.huggingface.co/v1" + PUBLIC_APP_ASSETS: "huggingchat" + PUBLIC_APP_NAME: "HuggingChat" + PUBLIC_APP_DESCRIPTION: "Making the community's best AI chat models available to everyone" + PUBLIC_ORIGIN: "https://huggingface.co" + PUBLIC_PLAUSIBLE_SCRIPT_URL: "https://plausible.io/js/pa-Io_oigECawqdlgpf5qvHb.js" + + TASK_MODEL: "Qwen/Qwen3-4B-Instruct-2507" + LLM_ROUTER_ARCH_BASE_URL: "https://router.huggingface.co/v1" + LLM_ROUTER_ROUTES_PATH: "build/client/chat/huggingchat/routes.chat.json" + LLM_ROUTER_ARCH_MODEL: "katanemo/Arch-Router-1.5B" + LLM_ROUTER_OTHER_ROUTE: "casual_conversation" + LLM_ROUTER_ARCH_TIMEOUT_MS: "10000" + LLM_ROUTER_ENABLE_MULTIMODAL: "true" + LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-235B-A22B-Thinking" + PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni" + PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png" + PUBLIC_LLM_ROUTER_ALIAS_ID: "omni" + MODELS: > + [ + { "id": "deepseek-ai/DeepSeek-V3.2-Exp", "description": "Experimental V3.2 release focused on faster, lower-cost inference with strong general reasoning and tool use." }, + { "id": "zai-org/GLM-4.6", "description": "Next-gen GLM with very long context and solid multilingual reasoning; good for agents and tools." }, + { "id": "Kwaipilot/KAT-Dev", "description": "Developer-oriented assistant tuned for coding, debugging, and lightweight agent workflows." }, + { "id": "Qwen/Qwen3-VL-235B-A22B-Instruct", "description": "Flagship multimodal Qwen (text+image) instruction model for high-accuracy visual reasoning and detailed explanations." }, + { "id": "deepseek-ai/DeepSeek-V3.1-Terminus", "description": "Refined V3.1 variant optimized for reliability on long contexts, structured outputs, and tool use." }, + { "id": "Qwen/Qwen3-VL-235B-A22B-Thinking", "description": "Deliberative multimodal Qwen that can produce step-wise visual+text reasoning traces for complex tasks." }, + { "id": "zai-org/GLM-4.6-FP8", "description": "FP8-optimized GLM-4.6 for faster/cheaper deployment with near-parity quality on most tasks." }, + { "id": "Qwen/Qwen3-235B-A22B-Thinking-2507", "description": "Deliberative text-only 235B Qwen variant for transparent, step-by-step reasoning on hard problems." }, + { "id": "Qwen/Qwen3-Next-80B-A3B-Instruct", "description": "Instruction tuned Qwen for multilingual reasoning, coding, long contexts." }, + { "id": "Qwen/Qwen3-Next-80B-A3B-Thinking", "description": "Thinking mode Qwen that outputs explicit step by step reasoning." }, + { "id": "moonshotai/Kimi-K2-Instruct-0905", "description": "Instruction MoE strong coding and multi step reasoning, long context." }, + { "id": "openai/gpt-oss-20b", "description": "Efficient open model for reasoning and tool use, runs locally." }, + { "id": "swiss-ai/Apertus-8B-Instruct-2509", "description": "Open, multilingual, trained on compliant data transparent global assistant." }, + { "id": "openai/gpt-oss-120b", "description": "High performing open model suitable for large scale applications." }, + { "id": "Qwen/Qwen3-Coder-30B-A3B-Instruct", "description": "Code specialized Qwen long context strong generation and function calling." }, + { "id": "meta-llama/Llama-3.1-8B-Instruct", "description": "Instruction tuned Llama efficient conversational assistant with improved alignment." }, + { "id": "Qwen/Qwen2.5-VL-7B-Instruct", "description": "Vision language Qwen handles images and text for basic multimodal tasks." }, + { "id": "Qwen/Qwen3-30B-A3B-Instruct-2507", "description": "Instruction tuned Qwen reliable general tasks with long context support." }, + { "id": "baidu/ERNIE-4.5-VL-28B-A3B-PT", "description": "Baidu multimodal MoE strong at complex vision language reasoning." }, + { "id": "baidu/ERNIE-4.5-0.3B-PT", "description": "Tiny efficient Baidu model surprisingly long context for lightweight chat." }, + { "id": "deepseek-ai/DeepSeek-R1", "description": "MoE reasoning model excels at math, logic, coding with steps." }, + { "id": "baidu/ERNIE-4.5-21B-A3B-PT", "description": "Efficient Baidu MoE competitive generation with fewer active parameters." }, + { "id": "swiss-ai/Apertus-70B-Instruct-2509", "description": "Open multilingual model trained on open data transparent and capable." }, + { "id": "Qwen/Qwen3-4B-Instruct-2507", "description": "Compact instruction Qwen great for lightweight assistants and apps." }, + { "id": "meta-llama/Llama-3.2-3B-Instruct", "description": "Small efficient Llama for basic conversations and instructions." }, + { "id": "Qwen/Qwen3-Coder-480B-A35B-Instruct", "description": "Huge Qwen coder repository scale understanding and advanced generation." }, + { "id": "meta-llama/Meta-Llama-3-8B-Instruct", "description": "Aligned, efficient Llama dependable open source assistant tasks." }, + { "id": "Qwen/Qwen3-4B-Thinking-2507", "description": "Small Qwen that emits transparent step by step reasoning." }, + { "id": "moonshotai/Kimi-K2-Instruct", "description": "MoE assistant strong coding, reasoning, agentic tasks, long context." }, + { "id": "zai-org/GLM-4.5V", "description": "Vision language MoE state of the art multimodal reasoning." }, + { "id": "zai-org/GLM-4.6", "description": "Hybrid reasoning model top choice for intelligent agent applications." }, + { "id": "deepseek-ai/DeepSeek-V3.1", "description": "Supports direct and thinking style reasoning within one model." }, + { "id": "Qwen/Qwen3-8B", "description": "Efficient Qwen assistant strong multilingual skills and formatting." }, + { "id": "Qwen/Qwen3-30B-A3B-Thinking-2507", "description": "Thinking mode Qwen explicit reasoning for complex interpretable tasks." }, + { "id": "google/gemma-3-27b-it", "description": "Multimodal Gemma long context strong text and image understanding." }, + { "id": "zai-org/GLM-4.5-Air", "description": "Efficient GLM strong reasoning and tool use at lower cost." }, + { "id": "HuggingFaceTB/SmolLM3-3B", "description": "Small multilingual long context model surprisingly strong reasoning." }, + { "id": "Qwen/Qwen3-30B-A3B", "description": "Qwen base model for general use or further fine tuning." }, + { "id": "Qwen/Qwen2.5-7B-Instruct", "description": "Compact instruction model solid for basic conversation and tasks." }, + { "id": "Qwen/Qwen3-32B", "description": "General purpose Qwen strong for complex queries and dialogues." }, + { "id": "Qwen/QwQ-32B", "description": "Preview Qwen showcasing next generation features and alignment." }, + { "id": "Qwen/Qwen3-235B-A22B-Instruct-2507", "description": "Flagship instruction Qwen near state of the art across domains." }, + { "id": "meta-llama/Llama-3.3-70B-Instruct", "description": "Improved Llama alignment and structure powerful complex conversations." }, + { "id": "Qwen/Qwen2.5-VL-32B-Instruct", "description": "Multimodal Qwen advanced visual reasoning for complex image plus text." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", "description": "Tiny distilled Qwen stepwise math and logic reasoning." }, + { "id": "Qwen/Qwen3-235B-A22B", "description": "Qwen base at flagship scale ideal for custom fine tuning." }, + { "id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "description": "Processes text and images excels at summarization and cross modal reasoning." }, + { "id": "NousResearch/Hermes-4-70B", "description": "Steerable assistant strong reasoning and creativity highly helpful." }, + { "id": "Qwen/Qwen2.5-Coder-32B-Instruct", "description": "Code model strong generation and tool use bridges sizes." }, + { "id": "katanemo/Arch-Router-1.5B", "description": "Lightweight router model directs queries to specialized backends." }, + { "id": "meta-llama/Llama-3.2-1B-Instruct", "description": "Ultra small Llama handles basic Q and A and instructions." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", "description": "Distilled Qwen excels at stepwise logic in compact footprint." }, + { "id": "deepseek-ai/DeepSeek-V3", "description": "General language model direct answers strong creative and knowledge tasks." }, + { "id": "deepseek-ai/DeepSeek-V3-0324", "description": "Updated V3 better reasoning and coding strong tool use." }, + { "id": "CohereLabs/command-a-translate-08-2025", "description": "Translation focused Command model high quality multilingual translation." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", "description": "Distilled from R1 strong reasoning standout dense model." }, + { "id": "baidu/ERNIE-4.5-VL-424B-A47B-Base-PT", "description": "Multimodal base text image pretraining for cross modal understanding." }, + { "id": "meta-llama/Llama-4-Maverick-17B-128E-Instruct", "description": "MoE multimodal Llama rivals top vision language models." }, + { "id": "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", "description": "Quantized giant coder faster lighter retains advanced code generation." }, + { "id": "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B", "description": "Qwen3 variant with R1 reasoning improvements compact and capable." }, + { "id": "deepseek-ai/DeepSeek-R1-0528", "description": "R1 update improved reasoning, fewer hallucinations, adds function calling.", "parameters": { "max_tokens": 32000 } }, + { "id": "Qwen/Qwen3-14B", "description": "Balanced Qwen good performance and efficiency for assistants." }, + { "id": "MiniMaxAI/MiniMax-M1-80k", "description": "Long context MoE very fast excels at long range reasoning and code." }, + { "id": "Qwen/Qwen2.5-Coder-7B-Instruct", "description": "Efficient coding assistant for lightweight programming tasks." }, + { "id": "aisingapore/Gemma-SEA-LION-v4-27B-IT", "description": "Gemma SEA LION optimized for Southeast Asian languages or enterprise." }, + { "id": "CohereLabs/aya-expanse-8b", "description": "Small Aya Expanse broad knowledge and efficient general reasoning." }, + { "id": "baichuan-inc/Baichuan-M2-32B", "description": "Medical reasoning specialist fine tuned for clinical QA bilingual." }, + { "id": "Qwen/Qwen2.5-VL-72B-Instruct", "description": "Vision language Qwen detailed image interpretation and instructions." }, + { "id": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", "description": "FP8 Maverick efficient deployment retains top multimodal capability." }, + { "id": "zai-org/GLM-4.1V-9B-Thinking", "description": "Vision language with explicit reasoning strong for its size." }, + { "id": "zai-org/GLM-4.5-Air-FP8", "description": "FP8 efficient GLM Air hybrid reasoning with minimal compute." }, + { "id": "google/gemma-2-2b-it", "description": "Small Gemma instruction tuned safe responsible outputs easy deployment." }, + { "id": "arcee-ai/AFM-4.5B", "description": "Enterprise focused model strong CPU performance compliant and practical." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", "description": "Llama distilled from R1 strong reasoning and structured outputs." }, + { "id": "CohereLabs/aya-vision-8b", "description": "Vision capable Aya handles images and text for basic multimodal." }, + { "id": "NousResearch/Hermes-3-Llama-3.1-405B", "description": "Highly aligned assistant excels at math, code, QA." }, + { "id": "Qwen/Qwen2.5-72B-Instruct", "description": "Accurate detailed instruction model supports tools and long contexts." }, + { "id": "meta-llama/Llama-Guard-4-12B", "description": "Safety guardrail model filters and enforces content policies." }, + { "id": "CohereLabs/command-a-vision-07-2025", "description": "Command model with image input captioning and visual QA." }, + { "id": "nvidia/Llama-3_1-Nemotron-Ultra-253B-v1", "description": "NVIDIA tuned Llama optimized throughput for research and production." }, + { "id": "meta-llama/Meta-Llama-3-70B-Instruct", "description": "Instruction tuned Llama improved reasoning and reliability over predecessors." }, + { "id": "NousResearch/Hermes-4-405B", "description": "Frontier Hermes hybrid reasoning excels at math, code, creativity." }, + { "id": "NousResearch/Hermes-2-Pro-Llama-3-8B", "description": "Small Hermes highly steerable maximized helpfulness for basics." }, + { "id": "google/gemma-2-9b-it", "description": "Gemma with improved accuracy and context safe, easy to deploy." }, + { "id": "Sao10K/L3-8B-Stheno-v3.2", "description": "Community Llama variant themed tuning and unique conversational style." }, + { "id": "deepcogito/cogito-v2-preview-llama-109B-MoE", "description": "MoE preview advanced reasoning tests DeepCogito v2 fine tuning." }, + { "id": "CohereLabs/c4ai-command-r-08-2024", "description": "Cohere Command variant instruction following with specialized tuning." }, + { "id": "baidu/ERNIE-4.5-300B-A47B-Base-PT", "description": "Large base model foundation for specialized language systems." }, + { "id": "CohereLabs/aya-expanse-32b", "description": "Aya Expanse large comprehensive knowledge and reasoning capabilities." }, + { "id": "CohereLabs/c4ai-command-a-03-2025", "description": "Updated Command assistant improved accuracy and general usefulness." }, + { "id": "CohereLabs/command-a-reasoning-08-2025", "description": "Command variant optimized for complex multi step logical reasoning." }, + { "id": "alpindale/WizardLM-2-8x22B", "description": "Multi expert WizardLM MoE approach for efficient high quality generation." }, + { "id": "tokyotech-llm/Llama-3.3-Swallow-70B-Instruct-v0.4", "description": "Academic fine tune potential multilingual and domain improvements." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", "description": "Llama distilled from R1 improved reasoning enterprise friendly." }, + { "id": "CohereLabs/c4ai-command-r7b-12-2024", "description": "Small Command variant research or regional adaptation focus." }, + { "id": "Sao10K/L3-70B-Euryale-v2.1", "description": "Creative community instruct model with distinctive persona." }, + { "id": "CohereLabs/aya-vision-32b", "description": "Larger Aya Vision advanced vision language with detailed reasoning." }, + { "id": "meta-llama/Llama-3.1-405B-Instruct", "description": "Massive instruction model very long context excels at complex tasks." }, + { "id": "CohereLabs/c4ai-command-r7b-arabic-02-2025", "description": "Command tuned for Arabic fluent and culturally appropriate outputs." }, + { "id": "Sao10K/L3-8B-Lunaris-v1", "description": "Community Llama creative role play oriented themed persona." }, + { "id": "Qwen/Qwen2.5-Coder-7B", "description": "Small Qwen coder basic programming assistance for low resource environments." }, + { "id": "Qwen/QwQ-32B-Preview", "description": "Preview Qwen experimental features and architecture refinements." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", "description": "Distilled Qwen mid size strong reasoning and clear steps." }, + { "id": "meta-llama/Llama-3.1-70B-Instruct", "description": "Instruction tuned Llama improved reasoning and factual reliability." }, + { "id": "Qwen/Qwen3-235B-A22B-FP8", "description": "FP8 quantized Qwen flagship efficient access to ultra large capabilities." }, + { "id": "zai-org/GLM-4-32B-0414", "description": "Open licensed GLM matches larger proprietary models on benchmarks." }, + { "id": "SentientAGI/Dobby-Unhinged-Llama-3.3-70B", "description": "Unfiltered candid creative outputs intentionally less restricted behavior." }, + { "id": "marin-community/marin-8b-instruct", "description": "Community tuned assistant helpful conversational everyday tasks." }, + { "id": "deepseek-ai/DeepSeek-Prover-V2-671B", "description": "Specialist for mathematical proofs and formal reasoning workflows." }, + { "id": "NousResearch/Hermes-3-Llama-3.1-70B", "description": "Highly aligned assistant strong complex instruction following." }, + { "id": "Qwen/Qwen2.5-Coder-3B-Instruct", "description": "Tiny coding assistant basic code completions and explanations." }, + { "id": "deepcogito/cogito-v2-preview-llama-70B", "description": "Preview fine tune enhanced reasoning and tool use indications." }, + { "id": "deepcogito/cogito-v2-preview-llama-405B", "description": "Preview at frontier scale tests advanced fine tuning methods." }, + { "id": "deepcogito/cogito-v2-preview-deepseek-671B-MoE", "description": "Experimental blend of DeepCogito and DeepSeek approaches for reasoning." } + ] + +infisical: + enabled: true + env: "ephemeral-us-east-1" + +replicas: 1 +autoscaling: + enabled: false + +resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 4 + memory: 8Gi diff --git a/chart/env/prod.yaml b/chart/env/prod.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bfcc28d0987fd25756921571520282e981a86403 --- /dev/null +++ b/chart/env/prod.yaml @@ -0,0 +1,218 @@ +image: + repository: huggingface + name: chat-ui + +nodeSelector: + role-huggingchat: "true" + +tolerations: + - key: "huggingface.co/huggingchat" + operator: "Equal" + value: "true" + effect: "NoSchedule" + +serviceAccount: + enabled: true + create: true + name: huggingchat-prod + +ingress: + path: "/chat" + annotations: + alb.ingress.kubernetes.io/healthcheck-path: "/chat/healthcheck" + alb.ingress.kubernetes.io/listen-ports: "[{\"HTTP\": 80}, {\"HTTPS\": 443}]" + alb.ingress.kubernetes.io/load-balancer-name: "hub-utils-prod-cloudfront" + alb.ingress.kubernetes.io/group.name: "hub-utils-prod-cloudfront" + alb.ingress.kubernetes.io/scheme: "internal" + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/tags: "Env=prod,Project=hub,Terraform=true" + alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30 + alb.ingress.kubernetes.io/target-type: "ip" + alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-1:707930574880:certificate/5b25b145-75db-4837-b9f3-7f238ba8a9c7,arn:aws:acm:us-east-1:707930574880:certificate/bfdf509c-f44b-400f-b9e1-6f7a861abe91" + kubernetes.io/ingress.class: "alb" + +ingressInternal: + enabled: true + path: "/chat" + annotations: + alb.ingress.kubernetes.io/healthcheck-path: "/chat/healthcheck" + alb.ingress.kubernetes.io/listen-ports: "[{\"HTTP\": 80}, {\"HTTPS\": 443}]" + alb.ingress.kubernetes.io/group.name: "hub-prod-internal-public" + alb.ingress.kubernetes.io/load-balancer-name: "hub-prod-internal-public" + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/tags: "Env=prod,Project=hub,Terraform=true" + alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30 + alb.ingress.kubernetes.io/target-type: "ip" + alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-1:707930574880:certificate/5b25b145-75db-4837-b9f3-7f238ba8a9c7,arn:aws:acm:us-east-1:707930574880:certificate/bfdf509c-f44b-400f-b9e1-6f7a861abe91" + kubernetes.io/ingress.class: "alb" + +envVars: + COUPLE_SESSION_WITH_COOKIE_NAME: "token" + OPENID_SCOPES: "openid profile inference-api" + USE_USER_TOKEN: "true" + AUTOMATIC_LOGIN: "false" + + ADDRESS_HEADER: "X-Forwarded-For" + APP_BASE: "/chat" + ALLOW_IFRAME: "false" + COOKIE_SAMESITE: "lax" + COOKIE_SECURE: "true" + EXPOSE_API: "true" + METRICS_ENABLED: "true" + LOG_LEVEL: "debug" + NODE_LOG_STRUCTURED_DATA: "true" + + OPENAI_BASE_URL: "https://router.huggingface.co/v1" + PUBLIC_APP_ASSETS: "huggingchat" + PUBLIC_APP_NAME: "HuggingChat" + PUBLIC_APP_DESCRIPTION: "Making the community's best AI chat models available to everyone" + PUBLIC_ORIGIN: "https://huggingface.co" + PUBLIC_PLAUSIBLE_SCRIPT_URL: "https://plausible.io/js/pa-Io_oigECawqdlgpf5qvHb.js" + + TASK_MODEL: "Qwen/Qwen3-4B-Instruct-2507" + LLM_ROUTER_ARCH_BASE_URL: "https://router.huggingface.co/v1" + LLM_ROUTER_ROUTES_PATH: "build/client/chat/huggingchat/routes.chat.json" + LLM_ROUTER_ARCH_MODEL: "katanemo/Arch-Router-1.5B" + LLM_ROUTER_OTHER_ROUTE: "casual_conversation" + LLM_ROUTER_ARCH_TIMEOUT_MS: "10000" + LLM_ROUTER_ENABLE_MULTIMODAL: "true" + LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-235B-A22B-Thinking" + PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni" + PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png" + PUBLIC_LLM_ROUTER_ALIAS_ID: "omni" + MODELS: > + [ + { "id": "deepseek-ai/DeepSeek-V3.2-Exp", "description": "Experimental V3.2 release focused on faster, lower-cost inference with strong general reasoning and tool use." }, + { "id": "zai-org/GLM-4.6", "description": "Next-gen GLM with very long context and solid multilingual reasoning; good for agents and tools." }, + { "id": "Kwaipilot/KAT-Dev", "description": "Developer-oriented assistant tuned for coding, debugging, and lightweight agent workflows." }, + { "id": "Qwen/Qwen3-VL-235B-A22B-Instruct", "description": "Flagship multimodal Qwen (text+image) instruction model for high-accuracy visual reasoning and detailed explanations." }, + { "id": "deepseek-ai/DeepSeek-V3.1-Terminus", "description": "Refined V3.1 variant optimized for reliability on long contexts, structured outputs, and tool use." }, + { "id": "Qwen/Qwen3-VL-235B-A22B-Thinking", "description": "Deliberative multimodal Qwen that can produce step-wise visual+text reasoning traces for complex tasks." }, + { "id": "zai-org/GLM-4.6-FP8", "description": "FP8-optimized GLM-4.6 for faster/cheaper deployment with near-parity quality on most tasks." }, + { "id": "Qwen/Qwen3-235B-A22B-Thinking-2507", "description": "Deliberative text-only 235B Qwen variant for transparent, step-by-step reasoning on hard problems." }, + { "id": "Qwen/Qwen3-Next-80B-A3B-Instruct", "description": "Instruction tuned Qwen for multilingual reasoning, coding, long contexts." }, + { "id": "Qwen/Qwen3-Next-80B-A3B-Thinking", "description": "Thinking mode Qwen that outputs explicit step by step reasoning." }, + { "id": "moonshotai/Kimi-K2-Instruct-0905", "description": "Instruction MoE strong coding and multi step reasoning, long context." }, + { "id": "openai/gpt-oss-20b", "description": "Efficient open model for reasoning and tool use, runs locally." }, + { "id": "swiss-ai/Apertus-8B-Instruct-2509", "description": "Open, multilingual, trained on compliant data transparent global assistant." }, + { "id": "openai/gpt-oss-120b", "description": "High performing open model suitable for large scale applications." }, + { "id": "Qwen/Qwen3-Coder-30B-A3B-Instruct", "description": "Code specialized Qwen long context strong generation and function calling." }, + { "id": "meta-llama/Llama-3.1-8B-Instruct", "description": "Instruction tuned Llama efficient conversational assistant with improved alignment." }, + { "id": "Qwen/Qwen2.5-VL-7B-Instruct", "description": "Vision language Qwen handles images and text for basic multimodal tasks." }, + { "id": "Qwen/Qwen3-30B-A3B-Instruct-2507", "description": "Instruction tuned Qwen reliable general tasks with long context support." }, + { "id": "baidu/ERNIE-4.5-VL-28B-A3B-PT", "description": "Baidu multimodal MoE strong at complex vision language reasoning." }, + { "id": "baidu/ERNIE-4.5-0.3B-PT", "description": "Tiny efficient Baidu model surprisingly long context for lightweight chat." }, + { "id": "deepseek-ai/DeepSeek-R1", "description": "MoE reasoning model excels at math, logic, coding with steps." }, + { "id": "baidu/ERNIE-4.5-21B-A3B-PT", "description": "Efficient Baidu MoE competitive generation with fewer active parameters." }, + { "id": "swiss-ai/Apertus-70B-Instruct-2509", "description": "Open multilingual model trained on open data transparent and capable." }, + { "id": "Qwen/Qwen3-4B-Instruct-2507", "description": "Compact instruction Qwen great for lightweight assistants and apps." }, + { "id": "meta-llama/Llama-3.2-3B-Instruct", "description": "Small efficient Llama for basic conversations and instructions." }, + { "id": "Qwen/Qwen3-Coder-480B-A35B-Instruct", "description": "Huge Qwen coder repository scale understanding and advanced generation." }, + { "id": "meta-llama/Meta-Llama-3-8B-Instruct", "description": "Aligned, efficient Llama dependable open source assistant tasks." }, + { "id": "Qwen/Qwen3-4B-Thinking-2507", "description": "Small Qwen that emits transparent step by step reasoning." }, + { "id": "moonshotai/Kimi-K2-Instruct", "description": "MoE assistant strong coding, reasoning, agentic tasks, long context." }, + { "id": "zai-org/GLM-4.5V", "description": "Vision language MoE state of the art multimodal reasoning." }, + { "id": "zai-org/GLM-4.6", "description": "Hybrid reasoning model top choice for intelligent agent applications." }, + { "id": "deepseek-ai/DeepSeek-V3.1", "description": "Supports direct and thinking style reasoning within one model." }, + { "id": "Qwen/Qwen3-8B", "description": "Efficient Qwen assistant strong multilingual skills and formatting." }, + { "id": "Qwen/Qwen3-30B-A3B-Thinking-2507", "description": "Thinking mode Qwen explicit reasoning for complex interpretable tasks." }, + { "id": "google/gemma-3-27b-it", "description": "Multimodal Gemma long context strong text and image understanding." }, + { "id": "zai-org/GLM-4.5-Air", "description": "Efficient GLM strong reasoning and tool use at lower cost." }, + { "id": "HuggingFaceTB/SmolLM3-3B", "description": "Small multilingual long context model surprisingly strong reasoning." }, + { "id": "Qwen/Qwen3-30B-A3B", "description": "Qwen base model for general use or further fine tuning." }, + { "id": "Qwen/Qwen2.5-7B-Instruct", "description": "Compact instruction model solid for basic conversation and tasks." }, + { "id": "Qwen/Qwen3-32B", "description": "General purpose Qwen strong for complex queries and dialogues." }, + { "id": "Qwen/QwQ-32B", "description": "Preview Qwen showcasing next generation features and alignment." }, + { "id": "Qwen/Qwen3-235B-A22B-Instruct-2507", "description": "Flagship instruction Qwen near state of the art across domains." }, + { "id": "meta-llama/Llama-3.3-70B-Instruct", "description": "Improved Llama alignment and structure powerful complex conversations." }, + { "id": "Qwen/Qwen2.5-VL-32B-Instruct", "description": "Multimodal Qwen advanced visual reasoning for complex image plus text." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", "description": "Tiny distilled Qwen stepwise math and logic reasoning." }, + { "id": "Qwen/Qwen3-235B-A22B", "description": "Qwen base at flagship scale ideal for custom fine tuning." }, + { "id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "description": "Processes text and images excels at summarization and cross modal reasoning." }, + { "id": "NousResearch/Hermes-4-70B", "description": "Steerable assistant strong reasoning and creativity highly helpful." }, + { "id": "Qwen/Qwen2.5-Coder-32B-Instruct", "description": "Code model strong generation and tool use bridges sizes." }, + { "id": "katanemo/Arch-Router-1.5B", "description": "Lightweight router model directs queries to specialized backends." }, + { "id": "meta-llama/Llama-3.2-1B-Instruct", "description": "Ultra small Llama handles basic Q and A and instructions." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", "description": "Distilled Qwen excels at stepwise logic in compact footprint." }, + { "id": "deepseek-ai/DeepSeek-V3", "description": "General language model direct answers strong creative and knowledge tasks." }, + { "id": "deepseek-ai/DeepSeek-V3-0324", "description": "Updated V3 better reasoning and coding strong tool use." }, + { "id": "CohereLabs/command-a-translate-08-2025", "description": "Translation focused Command model high quality multilingual translation." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", "description": "Distilled from R1 strong reasoning standout dense model." }, + { "id": "baidu/ERNIE-4.5-VL-424B-A47B-Base-PT", "description": "Multimodal base text image pretraining for cross modal understanding." }, + { "id": "meta-llama/Llama-4-Maverick-17B-128E-Instruct", "description": "MoE multimodal Llama rivals top vision language models." }, + { "id": "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", "description": "Quantized giant coder faster lighter retains advanced code generation." }, + { "id": "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B", "description": "Qwen3 variant with R1 reasoning improvements compact and capable." }, + { "id": "deepseek-ai/DeepSeek-R1-0528", "description": "R1 update improved reasoning, fewer hallucinations, adds function calling.", "parameters": { "max_tokens": 32000 } }, + { "id": "Qwen/Qwen3-14B", "description": "Balanced Qwen good performance and efficiency for assistants." }, + { "id": "MiniMaxAI/MiniMax-M1-80k", "description": "Long context MoE very fast excels at long range reasoning and code." }, + { "id": "Qwen/Qwen2.5-Coder-7B-Instruct", "description": "Efficient coding assistant for lightweight programming tasks." }, + { "id": "aisingapore/Gemma-SEA-LION-v4-27B-IT", "description": "Gemma SEA LION optimized for Southeast Asian languages or enterprise." }, + { "id": "CohereLabs/aya-expanse-8b", "description": "Small Aya Expanse broad knowledge and efficient general reasoning." }, + { "id": "baichuan-inc/Baichuan-M2-32B", "description": "Medical reasoning specialist fine tuned for clinical QA bilingual." }, + { "id": "Qwen/Qwen2.5-VL-72B-Instruct", "description": "Vision language Qwen detailed image interpretation and instructions." }, + { "id": "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", "description": "FP8 Maverick efficient deployment retains top multimodal capability." }, + { "id": "zai-org/GLM-4.1V-9B-Thinking", "description": "Vision language with explicit reasoning strong for its size." }, + { "id": "zai-org/GLM-4.5-Air-FP8", "description": "FP8 efficient GLM Air hybrid reasoning with minimal compute." }, + { "id": "google/gemma-2-2b-it", "description": "Small Gemma instruction tuned safe responsible outputs easy deployment." }, + { "id": "arcee-ai/AFM-4.5B", "description": "Enterprise focused model strong CPU performance compliant and practical." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", "description": "Llama distilled from R1 strong reasoning and structured outputs." }, + { "id": "CohereLabs/aya-vision-8b", "description": "Vision capable Aya handles images and text for basic multimodal." }, + { "id": "NousResearch/Hermes-3-Llama-3.1-405B", "description": "Highly aligned assistant excels at math, code, QA." }, + { "id": "Qwen/Qwen2.5-72B-Instruct", "description": "Accurate detailed instruction model supports tools and long contexts." }, + { "id": "meta-llama/Llama-Guard-4-12B", "description": "Safety guardrail model filters and enforces content policies." }, + { "id": "CohereLabs/command-a-vision-07-2025", "description": "Command model with image input captioning and visual QA." }, + { "id": "nvidia/Llama-3_1-Nemotron-Ultra-253B-v1", "description": "NVIDIA tuned Llama optimized throughput for research and production." }, + { "id": "meta-llama/Meta-Llama-3-70B-Instruct", "description": "Instruction tuned Llama improved reasoning and reliability over predecessors." }, + { "id": "NousResearch/Hermes-4-405B", "description": "Frontier Hermes hybrid reasoning excels at math, code, creativity." }, + { "id": "NousResearch/Hermes-2-Pro-Llama-3-8B", "description": "Small Hermes highly steerable maximized helpfulness for basics." }, + { "id": "google/gemma-2-9b-it", "description": "Gemma with improved accuracy and context safe, easy to deploy." }, + { "id": "Sao10K/L3-8B-Stheno-v3.2", "description": "Community Llama variant themed tuning and unique conversational style." }, + { "id": "deepcogito/cogito-v2-preview-llama-109B-MoE", "description": "MoE preview advanced reasoning tests DeepCogito v2 fine tuning." }, + { "id": "CohereLabs/c4ai-command-r-08-2024", "description": "Cohere Command variant instruction following with specialized tuning." }, + { "id": "baidu/ERNIE-4.5-300B-A47B-Base-PT", "description": "Large base model foundation for specialized language systems." }, + { "id": "CohereLabs/aya-expanse-32b", "description": "Aya Expanse large comprehensive knowledge and reasoning capabilities." }, + { "id": "CohereLabs/c4ai-command-a-03-2025", "description": "Updated Command assistant improved accuracy and general usefulness." }, + { "id": "CohereLabs/command-a-reasoning-08-2025", "description": "Command variant optimized for complex multi step logical reasoning." }, + { "id": "alpindale/WizardLM-2-8x22B", "description": "Multi expert WizardLM MoE approach for efficient high quality generation." }, + { "id": "tokyotech-llm/Llama-3.3-Swallow-70B-Instruct-v0.4", "description": "Academic fine tune potential multilingual and domain improvements." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", "description": "Llama distilled from R1 improved reasoning enterprise friendly." }, + { "id": "CohereLabs/c4ai-command-r7b-12-2024", "description": "Small Command variant research or regional adaptation focus." }, + { "id": "Sao10K/L3-70B-Euryale-v2.1", "description": "Creative community instruct model with distinctive persona." }, + { "id": "CohereLabs/aya-vision-32b", "description": "Larger Aya Vision advanced vision language with detailed reasoning." }, + { "id": "meta-llama/Llama-3.1-405B-Instruct", "description": "Massive instruction model very long context excels at complex tasks." }, + { "id": "CohereLabs/c4ai-command-r7b-arabic-02-2025", "description": "Command tuned for Arabic fluent and culturally appropriate outputs." }, + { "id": "Sao10K/L3-8B-Lunaris-v1", "description": "Community Llama creative role play oriented themed persona." }, + { "id": "Qwen/Qwen2.5-Coder-7B", "description": "Small Qwen coder basic programming assistance for low resource environments." }, + { "id": "Qwen/QwQ-32B-Preview", "description": "Preview Qwen experimental features and architecture refinements." }, + { "id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", "description": "Distilled Qwen mid size strong reasoning and clear steps." }, + { "id": "meta-llama/Llama-3.1-70B-Instruct", "description": "Instruction tuned Llama improved reasoning and factual reliability." }, + { "id": "Qwen/Qwen3-235B-A22B-FP8", "description": "FP8 quantized Qwen flagship efficient access to ultra large capabilities." }, + { "id": "zai-org/GLM-4-32B-0414", "description": "Open licensed GLM matches larger proprietary models on benchmarks." }, + { "id": "SentientAGI/Dobby-Unhinged-Llama-3.3-70B", "description": "Unfiltered candid creative outputs intentionally less restricted behavior." }, + { "id": "marin-community/marin-8b-instruct", "description": "Community tuned assistant helpful conversational everyday tasks." }, + { "id": "deepseek-ai/DeepSeek-Prover-V2-671B", "description": "Specialist for mathematical proofs and formal reasoning workflows." }, + { "id": "NousResearch/Hermes-3-Llama-3.1-70B", "description": "Highly aligned assistant strong complex instruction following." }, + { "id": "Qwen/Qwen2.5-Coder-3B-Instruct", "description": "Tiny coding assistant basic code completions and explanations." }, + { "id": "deepcogito/cogito-v2-preview-llama-70B", "description": "Preview fine tune enhanced reasoning and tool use indications." }, + { "id": "deepcogito/cogito-v2-preview-llama-405B", "description": "Preview at frontier scale tests advanced fine tuning methods." }, + { "id": "deepcogito/cogito-v2-preview-deepseek-671B-MoE", "description": "Experimental blend of DeepCogito and DeepSeek approaches for reasoning." } + ] + +infisical: + enabled: true + env: "prod-us-east-1" + +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 30 + targetMemoryUtilizationPercentage: "50" + targetCPUUtilizationPercentage: "50" + +resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 4 + memory: 8Gi diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..eee5a181d225c2aff53344c446288240d37d3d0b --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,22 @@ +{{- define "name" -}} +{{- default $.Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "app.name" -}} +chat-ui +{{- end -}} + +{{- define "labels.standard" -}} +release: {{ $.Release.Name | quote }} +heritage: {{ $.Release.Service | quote }} +chart: "{{ include "name" . }}" +app: "{{ include "app.name" . }}" +{{- end -}} + +{{- define "labels.resolver" -}} +release: {{ $.Release.Name | quote }} +heritage: {{ $.Release.Service | quote }} +chart: "{{ include "name" . }}" +app: "{{ include "app.name" . }}-resolver" +{{- end -}} + diff --git a/chart/templates/config.yaml b/chart/templates/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c4c803e9e5f8b473ae216d5a2933cb67d46bc011 --- /dev/null +++ b/chart/templates/config.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +data: + {{- range $key, $value := $.Values.envVars }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d3d69cdee04646aeb6c013cc2176ee4bbc128f3b --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} + {{- if .Values.infisical.enabled }} + annotations: + secrets.infisical.com/auto-reload: "true" + {{- end }} +spec: + progressDeadlineSeconds: 600 + {{- if not $.Values.autoscaling.enabled }} + replicas: {{ .Values.replicas }} + {{- end }} + revisionHistoryLimit: 10 + selector: + matchLabels: {{ include "labels.standard" . | nindent 6 }} + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: {{ include "labels.standard" . | nindent 8 }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/config.yaml") . | sha256sum }} + {{- if $.Values.envVars.NODE_LOG_STRUCTURED_DATA }} + co.elastic.logs/json.expand_keys: "true" + {{- end }} + spec: + {{- if .Values.serviceAccount.enabled }} + serviceAccountName: "{{ .Values.serviceAccount.name | default (include "name" .) }}" + {{- end }} + containers: + - name: chat-ui + image: "{{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + readinessProbe: + failureThreshold: 30 + periodSeconds: 10 + httpGet: + path: {{ $.Values.envVars.APP_BASE | default "" }}/healthcheck + port: {{ $.Values.envVars.APP_PORT | default 3000 | int }} + livenessProbe: + failureThreshold: 30 + periodSeconds: 10 + httpGet: + path: {{ $.Values.envVars.APP_BASE | default "" }}/healthcheck + port: {{ $.Values.envVars.APP_PORT | default 3000 | int }} + ports: + - containerPort: {{ $.Values.envVars.APP_PORT | default 3000 | int }} + name: http + protocol: TCP + {{- if eq "true" $.Values.envVars.METRICS_ENABLED }} + - containerPort: {{ $.Values.envVars.METRICS_PORT | default 5565 | int }} + name: metrics + protocol: TCP + {{- end }} + resources: {{ toYaml .Values.resources | nindent 12 }} + {{- with $.Values.extraEnv }} + env: + {{- toYaml . | nindent 14 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "name" . }} + {{- if $.Values.infisical.enabled }} + - secretRef: + name: {{ include "name" $ }}-secs + {{- end }} + {{- with $.Values.extraEnvFrom }} + {{- toYaml . | nindent 14 }} + {{- end }} + nodeSelector: {{ toYaml .Values.nodeSelector | nindent 8 }} + tolerations: {{ toYaml .Values.tolerations | nindent 8 }} + volumes: + - name: config + configMap: + name: {{ include "name" . }} diff --git a/chart/templates/hpa.yaml b/chart/templates/hpa.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bf7bd3b256b79c54269ae39afb02c816878596dc --- /dev/null +++ b/chart/templates/hpa.yaml @@ -0,0 +1,45 @@ +{{- if $.Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "name" . }} + minReplicas: {{ $.Values.autoscaling.minReplicas }} + maxReplicas: {{ $.Values.autoscaling.maxReplicas }} + metrics: + {{- if ne "" $.Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $.Values.autoscaling.targetMemoryUtilizationPercentage | int }} + {{- end }} + {{- if ne "" $.Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $.Values.autoscaling.targetCPUUtilizationPercentage | int }} + {{- end }} + behavior: + scaleDown: + stabilizationWindowSeconds: 600 + policies: + - type: Percent + value: 10 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 0 + policies: + - type: Pods + value: 1 + periodSeconds: 30 +{{- end }} diff --git a/chart/templates/infisical.yaml b/chart/templates/infisical.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6a11e084f6e0ab300ea4ec2b2694a79dadc1bdf8 --- /dev/null +++ b/chart/templates/infisical.yaml @@ -0,0 +1,24 @@ +{{- if .Values.infisical.enabled }} +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: {{ include "name" $ }}-infisical-secret + namespace: {{ $.Release.Namespace }} +spec: + authentication: + universalAuth: + credentialsRef: + secretName: {{ .Values.infisical.operatorSecretName | quote }} + secretNamespace: {{ .Values.infisical.operatorSecretNamespace | quote }} + secretsScope: + envSlug: {{ .Values.infisical.env | quote }} + projectSlug: {{ .Values.infisical.project | quote }} + secretsPath: / + hostAPI: {{ .Values.infisical.url | quote }} + managedSecretReference: + creationPolicy: Owner + secretName: {{ include "name" $ }}-secs + secretNamespace: {{ .Release.Namespace | quote }} + secretType: Opaque + resyncInterval: {{ .Values.infisical.resyncInterval }} +{{- end }} diff --git a/chart/templates/ingress-internal.yaml b/chart/templates/ingress-internal.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bf87d0b6c960871327908e243e79e05475b825d5 --- /dev/null +++ b/chart/templates/ingress-internal.yaml @@ -0,0 +1,32 @@ +{{- if $.Values.ingressInternal.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: {{ toYaml .Values.ingressInternal.annotations | nindent 4 }} + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }}-internal + namespace: {{ .Release.Namespace }} +spec: + {{ if $.Values.ingressInternal.className }} + ingressClassName: {{ .Values.ingressInternal.className }} + {{ end }} + {{- with .Values.ingressInternal.tls }} + tls: + - hosts: + - {{ $.Values.domain | quote }} + {{- with .secretName }} + secretName: {{ . }} + {{- end }} + {{- end }} + rules: + - host: {{ .Values.domain }} + http: + paths: + - backend: + service: + name: {{ include "name" . }} + port: + name: http + path: {{ $.Values.ingressInternal.path | default "/" }} + pathType: Prefix +{{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8ba4e8a4055f23471b77597dcfc956dc811547e0 --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,32 @@ +{{- if $.Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: {{ toYaml .Values.ingress.annotations | nindent 4 }} + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + {{ if $.Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{ end }} + {{- with .Values.ingress.tls }} + tls: + - hosts: + - {{ $.Values.domain | quote }} + {{- with .secretName }} + secretName: {{ . }} + {{- end }} + {{- end }} + rules: + - host: {{ .Values.domain }} + http: + paths: + - backend: + service: + name: {{ include "name" . }} + port: + name: http + path: {{ $.Values.ingress.path | default "/" }} + pathType: Prefix +{{- end }} diff --git a/chart/templates/network-policy.yaml b/chart/templates/network-policy.yaml new file mode 100644 index 0000000000000000000000000000000000000000..59f5df5893a97f4075237ac7cdb4979dce7298a9 --- /dev/null +++ b/chart/templates/network-policy.yaml @@ -0,0 +1,36 @@ +{{- if $.Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + egress: + - ports: + - port: 53 + protocol: UDP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + - to: + {{- range $ip := .Values.networkPolicy.allowedBlocks }} + - ipBlock: + cidr: {{ $ip | quote }} + {{- end }} + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 169.254.169.254/32 + podSelector: + matchLabels: {{ include "labels.standard" . | nindent 6 }} + policyTypes: + - Egress +{{- end }} diff --git a/chart/templates/service-account.yaml b/chart/templates/service-account.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fc3a184c9def4cef61836f8eac152ab61fe4d047 --- /dev/null +++ b/chart/templates/service-account.yaml @@ -0,0 +1,13 @@ +{{- if and .Values.serviceAccount.enabled .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +metadata: + name: "{{ .Values.serviceAccount.name | default (include "name" .) }}" + namespace: {{ .Release.Namespace }} + labels: {{ include "labels.standard" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/chart/templates/service-monitor.yaml b/chart/templates/service-monitor.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0c8e4dab423946a318a3daee7bd4dfcab0cee151 --- /dev/null +++ b/chart/templates/service-monitor.yaml @@ -0,0 +1,17 @@ +{{- if eq "true" $.Values.envVars.METRICS_ENABLED }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: {{ include "labels.standard" . | nindent 4 }} + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} +spec: + selector: + matchLabels: {{ include "labels.standard" . | nindent 6 }} + endpoints: + - port: metrics + path: /metrics + interval: 10s + scheme: http + scrapeTimeout: 10s +{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ef364f0926861b9e934e9830d7884cdd63ecd6ec --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ include "name" . }}" + annotations: {{ toYaml .Values.service.annotations | nindent 4 }} + namespace: {{ .Release.Namespace }} + labels: {{ include "labels.standard" . | nindent 4 }} +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + {{- if eq "true" $.Values.envVars.METRICS_ENABLED }} + - name: metrics + port: {{ $.Values.envVars.METRICS_PORT | default 5565 | int }} + protocol: TCP + targetPort: metrics + {{- end }} + selector: {{ include "labels.standard" . | nindent 4 }} + type: {{.Values.service.type}} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..29446ac9f489ccfc8b0f940d729438b55b90995b --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,73 @@ +image: + repository: ghcr.io/huggingface + name: chat-ui + tag: 0.0.0-latest + pullPolicy: IfNotPresent + +replicas: 3 + +domain: huggingface.co + +networkPolicy: + enabled: false + allowedBlocks: [] + +service: + type: NodePort + annotations: { } + +serviceAccount: + enabled: false + create: false + name: "" + automountServiceAccountToken: true + annotations: { } + +ingress: + enabled: true + path: "/" + annotations: { } + # className: "nginx" + tls: { } + # secretName: XXX + +ingressInternal: + enabled: false + path: "/" + annotations: { } + # className: "nginx" + tls: { } + +resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi +nodeSelector: {} +tolerations: [] + +envVars: { } + +infisical: + enabled: false + env: "" + project: "huggingchat-v2-a1" + url: "" + resyncInterval: 60 + operatorSecretName: "huggingchat-operator-secrets" + operatorSecretNamespace: "hub-utils" + +# Allow to environment injections on top or instead of infisical +extraEnvFrom: [] +extraEnv: [] + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 2 + targetMemoryUtilizationPercentage: "" + targetCPUUtilizationPercentage: "" + +## Metrics removed; monitoring configuration no longer used diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..f74aea158ba500ee47ba020f209b0e43b3c787cc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +# For development only +# Set MONGODB_URL=mongodb://localhost:27017 in .env.local to use this container +services: + mongo: + image: mongo:8 + hostname: mongodb + ports: + - ${LOCAL_MONGO_PORT:-27017}:27017 + command: --replSet rs0 --bind_ip_all #--setParameter notablescan=1 + mem_limit: "5g" + mem_reservation: "3g" + healthcheck: + # need to specify the hostname here because the default is the container name, and we run the app outside of docker + test: test $$(mongosh --quiet --eval 'try {rs.status().ok} catch(e) {rs.initiate({_id:"rs0",members:[{_id:0,host:"127.0.0.1:${LOCAL_MONGO_PORT:-27017}"}]}).ok}') -eq 1 + interval: 5s + volumes: + - mongodb-data:/data/db + restart: always + +volumes: + mongodb-data: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..c1fea7a273164c087dd3da31d3f04998188e7f57 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,19 @@ +ENV_LOCAL_PATH=/app/.env.local + +if test -z "${DOTENV_LOCAL}" ; then + if ! test -f "${ENV_LOCAL_PATH}" ; then + echo "DOTENV_LOCAL was not found in the ENV variables and .env.local is not set using a bind volume. Make sure to set environment variables properly. " + fi; +else + echo "DOTENV_LOCAL was found in the ENV variables. Creating .env.local file." + cat <<< "$DOTENV_LOCAL" > ${ENV_LOCAL_PATH} +fi; + +if [ "$INCLUDE_DB" = "true" ] ; then + echo "Starting local MongoDB instance" + nohup mongod & +fi; + +export PUBLIC_VERSION=$(node -p "require('./package.json').version") + +dotenv -e /app/.env -c -- node --dns-result-order=ipv4first /app/build/index.js -- --host 0.0.0.0 --port 3000 \ No newline at end of file diff --git a/models/add-your-models-here.txt b/models/add-your-models-here.txt new file mode 100644 index 0000000000000000000000000000000000000000..7086be91e7be1b995adbdb3d660946c3c11e4061 --- /dev/null +++ b/models/add-your-models-here.txt @@ -0,0 +1 @@ +You can add .gguf files to this folder, and they will be picked up automatically by chat-ui. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..687c65f1a6569522ab639d95dbaaf225bacb6093 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11824 @@ +{ + "name": "chat-ui", + "version": "0.20.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-ui", + "version": "0.20.0", + "dependencies": { + "@aws-sdk/credential-providers": "^3.925.0", + "@elysiajs/swagger": "^1.3.0", + "@huggingface/hub": "^2.2.0", + "@huggingface/inference": "^4.11.3", + "@iconify-json/bi": "^1.1.21", + "@resvg/resvg-js": "^2.6.2", + "autoprefixer": "^10.4.14", + "aws4": "^1.13.2", + "bits-ui": "^2.14.2", + "date-fns": "^2.29.3", + "dotenv": "^16.5.0", + "file-type": "^21.0.0", + "handlebars": "^4.7.8", + "highlight.js": "^11.7.0", + "husky": "^9.0.11", + "ip-address": "^9.0.5", + "jsdom": "^22.0.0", + "json5": "^2.2.3", + "katex": "^0.16.21", + "lint-staged": "^15.2.7", + "marked": "^12.0.1", + "mime-types": "^2.1.35", + "mongodb": "^5.8.0", + "nanoid": "^5.0.9", + "openai": "^4.44.0", + "openid-client": "^5.4.2", + "parquetjs": "^0.11.2", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + "postcss": "^8.4.31", + "prom-client": "^15.1.3", + "satori": "^0.10.11", + "satori-html": "^0.3.2", + "sharp": "^0.33.4", + "tailwind-scrollbar": "^3.0.0", + "tailwindcss": "^3.4.0", + "undici": "^7.16.0", + "uuid": "^10.0.0", + "vitest-browser-svelte": "^0.1.0", + "zod": "^3.22.3" + }, + "devDependencies": { + "@elysiajs/eden": "^1.3.2", + "@faker-js/faker": "^8.4.1", + "@iconify-json/carbon": "^1.1.16", + "@iconify-json/eos-icons": "^1.1.6", + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.21.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/typography": "^0.5.9", + "@types/dompurify": "^3.0.5", + "@types/js-yaml": "^4.0.9", + "@types/jsdom": "^21.1.1", + "@types/katex": "^0.16.7", + "@types/mime-types": "^2.1.4", + "@types/minimist": "^1.2.5", + "@types/node": "^22.1.0", + "@types/parquetjs": "^0.10.3", + "@types/uuid": "^9.0.8", + "@types/yazl": "^3.3.0", + "@typescript-eslint/eslint-plugin": "^6.x", + "@typescript-eslint/parser": "^6.x", + "bson-objectid": "^2.0.4", + "dompurify": "^3.2.4", + "elysia": "^1.3.2", + "eslint": "^8.28.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-svelte": "^2.45.1", + "isomorphic-dompurify": "2.13.0", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "mongodb-memory-server": "^10.1.2", + "playwright": "^1.55.1", + "prettier": "^3.5.3", + "prettier-plugin-svelte": "^3.2.6", + "prettier-plugin-tailwindcss": "^0.6.11", + "sade": "^1.8.1", + "superjson": "^2.2.2", + "svelte": "^5.33.3", + "svelte-check": "^4.0.0", + "tslib": "^2.4.1", + "typescript": "^5.5.0", + "unplugin-icons": "^0.16.1", + "vite": "^6.3.5", + "vite-node": "^3.0.9", + "vitest": "^3.1.4", + "yazl": "^3.3.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz", + "integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "find-up": "^5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/install-pkg/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@antfu/install-pkg/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/install-pkg/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@antfu/install-pkg/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@antfu/install-pkg/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@antfu/install-pkg/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/install-pkg/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@antfu/install-pkg/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.925.0.tgz", + "integrity": "sha512-7koO8MTU6T0dKAaFi7Bm06t4l8M9z798WSvpwzcCVItf6UAj+popz5MKzomxpd4Ire7C1jqqponiM8rrxNyYcQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-node": "3.925.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.925.0.tgz", + "integrity": "sha512-ixC9CyXe/mBo1X+bzOxIIzsdBYzM+klWoHUYzwnPMrXhpDrMjj8D24R/FPqrDnhoYYXiyS4BApRLpeymsFJq2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.925.0.tgz", + "integrity": "sha512-hSA6PE/u+DYYJVJ01cyKiDR3d31kOJ1l+qJJimEiG+jH1K+EUgjhNVZKHUzEbumVvpWVHeZJ7Hs6iq4F/rS4+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", + "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", + "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.925.0.tgz", + "integrity": "sha512-TOs/UkKWwXrSPolRTChpDUQjczw6KqbbanF0EzjUm3sp/AS1ThOQCKuTTdaOBZXkCIJdvRmZjF3adccE3rAoXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.925.0", + "@aws-sdk/credential-provider-web-identity": "3.925.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.925.0.tgz", + "integrity": "sha512-+T9mnnTY73MLkVxsk5RtzE4fv7GnMhR7iXhL/yTusf1zLfA09uxlA9VCz6tWxm5rHcO4ZN0x4hnqqDhM+DB5KQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.925.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.925.0", + "@aws-sdk/credential-provider-web-identity": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.925.0.tgz", + "integrity": "sha512-aZlUC6LRsOMDvIu0ifF62mTjL3KGzclWu5XBBN8eLDAYTdhqMxv3HyrqWoiHnGZnZGaVU+II+qsVoeBnGOwHow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.925.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.925.0.tgz", + "integrity": "sha512-dR34s8Sfd1wJBzIuvRFO2FCnLmYD8iwPWrdXWI2ZypFt1EQR8jeQ20mnS+UOCoR5Z0tY6wJqEgTXKl4KuZ+DUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.925.0.tgz", + "integrity": "sha512-CTTFn+8NiXRoyKbaTKXCSZ9pUs3R3HllTgl2In8Mxl60Eim9QrP3QYbSjH+pqaIOf1qhbe1UuEICzGrO3Y+8MA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.925.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-cognito-identity": "3.925.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.925.0", + "@aws-sdk/credential-provider-node": "3.925.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.925.0", + "@aws-sdk/credential-provider-web-identity": "3.925.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.925.0.tgz", + "integrity": "sha512-Fc8QhH+1YzGQb5aWQUX6gRnKSzUZ9p3p/muqXIgYBL8RSd5O6hSPhDTyrOWE247zFlOjVlAlEnoTMJKarH0cIA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.925.0.tgz", + "integrity": "sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.925.0.tgz", + "integrity": "sha512-F4Oibka1W5YYDeL+rGt/Hg3NLjOzrJdmuZOE0OFQt/U6dnJwYmYi2gFqduvZnZcD1agNm37mh7/GUq1zvKS6ig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", + "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-endpoints": "^3.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", + "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", + "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.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==", + "license": "MIT", + "peer": true, + "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/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==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@elysiajs/eden": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@elysiajs/eden/-/eden-1.3.2.tgz", + "integrity": "sha512-0bCU5DO7J7hQfS2y3O3399GtoxMWRDMgQNMTHOnf70/F2nF8SwGHvzwh3+wO62Ko5FMF7EYqTN9Csw/g/Q7qwg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "elysia": ">= 1.3.0" + } + }, + "node_modules/@elysiajs/swagger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@elysiajs/swagger/-/swagger-1.3.0.tgz", + "integrity": "sha512-0fo3FWkDRPNYpowJvLz3jBHe9bFe6gruZUyf+feKvUEEMG9ZHptO1jolSoPE0ffFw1BgN1/wMsP19p4GRXKdfg==", + "license": "MIT", + "dependencies": { + "@scalar/themes": "^0.9.52", + "@scalar/types": "^0.0.12", + "openapi-types": "^12.1.3", + "pathe": "^1.1.2" + }, + "peerDependencies": { + "elysia": ">= 1.3.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "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, + "license": "MIT", + "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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@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, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@huggingface/hub": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.2.0.tgz", + "integrity": "sha512-G+VS1eMp80KovIHBlsiEigS6I6qmI4j+VQ1UZ8CaXT+pw2A7tj6e/crfxFdKNE2uOK5oQkRFiCBJykMwrWQ8OA==", + "license": "MIT", + "dependencies": { + "@huggingface/tasks": "^0.19.11" + }, + "bin": { + "hfjs": "dist/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/inference": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.11.3.tgz", + "integrity": "sha512-Fqpj89DuB6i9j+cos9i0bfUKlpx5NFFsmvED0OAdE1gUSTHR86GpUZ0xkKy58IYXV1yFyHLFxQaOn0XDmD2m7Q==", + "license": "MIT", + "dependencies": { + "@huggingface/jinja": "^0.5.1", + "@huggingface/tasks": "^0.19.52" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.1.tgz", + "integrity": "sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tasks": { + "version": "0.19.52", + "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.52.tgz", + "integrity": "sha512-ERporbPcWOeeN22PG3UoC3n/kgk50/Gn03A1NPwO2fqlzaP01ADug0DazPi8W3HandT6LHycv7tAjo+sCOBRtw==", + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/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, + "license": "BSD-3-Clause" + }, + "node_modules/@iconify-json/bi": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@iconify-json/bi/-/bi-1.2.4.tgz", + "integrity": "sha512-ipD8nm86ovjgXGEJj/B5oSJGaEIsGgzrKqNT1ei66nRExzK6Mgh4an/efG30Xtvp2eQjz9eWN5kHmadbnjUmzw==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/carbon": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.9.tgz", + "integrity": "sha512-RyeSAuFTzhs1GX4yrzVEKEbNQGt95p9zMR4S2F63vbThtNoUr5OKwaWbhO/GbHQCSgdbKuZv2ApAOsY2fLxLbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/eos-icons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@iconify-json/eos-icons/-/eos-icons-1.2.2.tgz", + "integrity": "sha512-kYfV1WfgiHDbWdG9JEbV1K77MvksRmo9KIM4VjtYFMnF8pKqTv4MoLIOdCD2lbRlhBZspxr1GKde5Z/LYqz3Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@iconify/utils/node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils/node_modules/pkg-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", + "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.1", + "exsolve": "^1.0.1", + "pathe": "^2.0.3" + } + }, + "node_modules/@iconify/utils/node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@internationalized/date": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz", + "integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "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/@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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?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==", + "license": "MIT" + }, + "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==", + "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/@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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "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/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", + "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scalar/openapi-types": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.1.tgz", + "integrity": "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/themes": { + "version": "0.9.86", + "resolved": "https://registry.npmjs.org/@scalar/themes/-/themes-0.9.86.tgz", + "integrity": "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row==", + "license": "MIT", + "dependencies": { + "@scalar/types": "0.1.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/themes/node_modules/@scalar/openapi-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.2.0.tgz", + "integrity": "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA==", + "license": "MIT", + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/themes/node_modules/@scalar/types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.1.7.tgz", + "integrity": "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw==", + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.2.0", + "@unhead/schema": "^1.11.11", + "nanoid": "^5.1.5", + "type-fest": "^4.20.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/themes/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@scalar/types": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.12.tgz", + "integrity": "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ==", + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.1.1", + "@unhead/schema": "^1.9.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@shuding/opentype.js/node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.33", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.33.tgz", + "integrity": "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g==", + "license": "MIT", + "optional": true + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.4.tgz", + "integrity": "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.2.tgz", + "integrity": "sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.2.tgz", + "integrity": "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.4.tgz", + "integrity": "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.5.tgz", + "integrity": "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.4.tgz", + "integrity": "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.4.tgz", + "integrity": "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.4.tgz", + "integrity": "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.6.tgz", + "integrity": "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.2", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-middleware": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.6.tgz", + "integrity": "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/service-error-classification": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.4.tgz", + "integrity": "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.4.tgz", + "integrity": "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.4.tgz", + "integrity": "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.4.tgz", + "integrity": "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.4.tgz", + "integrity": "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.4.tgz", + "integrity": "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.4.tgz", + "integrity": "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.4.tgz", + "integrity": "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.4.tgz", + "integrity": "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.4.tgz", + "integrity": "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.4.tgz", + "integrity": "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.2.tgz", + "integrity": "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.2", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.1.tgz", + "integrity": "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.4.tgz", + "integrity": "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.5.tgz", + "integrity": "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.8.tgz", + "integrity": "sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.2", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.4.tgz", + "integrity": "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.4.tgz", + "integrity": "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.4.tgz", + "integrity": "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.5.tgz", + "integrity": "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.21.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.2.tgz", + "integrity": "sha512-EMYTY4+rNa7TaRZYzCqhQslEkACEZzWc363jOYuc90oJrgvlWTcgqTxcGSIJim48hPaXwYlHyatRnnMmTFf5tA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", + "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "license": "MIT" + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-int64": { + "version": "0.4.32", + "resolved": "https://registry.npmjs.org/@types/node-int64/-/node-int64-0.4.32.tgz", + "integrity": "sha512-xf/JsSlnXQ+mzvc0IpXemcrO4BrCfpgNpMco+GLcXkFk01k/gW9lGJu+Vof0ZSvHK6DsHJDPSbjFPs36QkWXqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parquetjs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@types/parquetjs/-/parquetjs-0.10.6.tgz", + "integrity": "sha512-ZCsD6j97YD0mGU8/VnVs3NjORXa7zeHvqlpJpCqy4jU8a1O21dalL+MFn9QNbdEfy8rszR1N7NHeT7/LdtHf+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-int64": "*" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/yazl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-3.3.0.tgz", + "integrity": "sha512-mFL6lGkk2N5u5nIxpNV/K5LW3qVSbxhJrMxYGOOxZndWxMgCamr/iCsq/1t9kd8pEwhuNP91LC5qZm/qS9pOEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unhead/schema": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.20.tgz", + "integrity": "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==", + "license": "MIT", + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@vitest/browser": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.2.tgz", + "integrity": "sha512-LJk8ZhCGhgG6G6jFFJ9LX83ibRY8FszLuu9zPaYFDrcHbBwNXwt1v06HRs/vHVYxwjw3/BGzSIgn9Et2P6rCiA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.6.1", + "@vitest/mocker": "3.2.2", + "@vitest/utils": "3.2.2", + "magic-string": "^0.30.17", + "sirv": "^3.0.1", + "tinyrainbow": "^2.0.0", + "ws": "^8.18.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "3.2.2", + "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.2.tgz", + "integrity": "sha512-ipHw0z669vEMjzz3xQE8nJX1s0rQIb7oEl4jjl35qWTwm/KIHERIg/p/zORrjAaZKXfsv7IybcNGHwhOOAPMwQ==", + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.2", + "@vitest/utils": "3.2.2", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.2.tgz", + "integrity": "sha512-jKojcaRyIYpDEf+s7/dD3LJt53c0dPfp5zCPXz9H/kcGrSlovU/t1yEaNzM9oFME3dcd4ULwRI/x0Po1Zf+LTw==", + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.2", + "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/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.2.tgz", + "integrity": "sha512-FY4o4U1UDhO9KMd2Wee5vumwcaHw7Vg4V7yR4Oq6uK34nhEJOmdRYrk3ClburPRUA09lXD/oXWZ8y/Sdma0aUQ==", + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.2.tgz", + "integrity": "sha512-GYcHcaS3ejGRZYed2GAkvsjBeXIEerDKdX3orQrBJqLRiea4NSS9qvn9Nxmuy1IwIB+EjFOaxXnX79l8HFaBwg==", + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.2.tgz", + "integrity": "sha512-aMEI2XFlR1aNECbBs5C5IZopfi5Lb8QJZGGpzS8ZUHML5La5wCbrbhLOVSME68qwpT05ROEEOAZPRXFpxZV2wA==", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.2.tgz", + "integrity": "sha512-6Utxlx3o7pcTxvp0u8kUiXtRFScMrUg28KjB3R2hon7w4YqOFAEA9QwzPVVS1QNL3smo4xRNOpNZClRVfpMcYg==", + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.2.tgz", + "integrity": "sha512-qJYMllrWpF/OYfWHP32T31QCaLa3BAzT/n/8mNGhPdVcjY+JYazQFO1nsJvXU12Kp1xMpNY4AGuljPTNjQve6A==", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.2", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "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/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha512-u4cBQNepWxYA55FunZSM7wMi55yQaN0otnhhilNoWHq0MfOfJeQx0v0mRRpolGOExPjZcl6FtB0BB8Xkb88F0g==", + "license": "MIT", + "optional": true + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, + "node_modules/bits-ui": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.2.tgz", + "integrity": "sha512-YqpAJj/nRTZjf7IlgUC3QlepVZ7YFiAQWpZaYUOAZFW5Py+g5DYkhEDTdNFI5SReo7l1rct/nRpMK4pfL9Xffw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "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": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/bson-objectid": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/bson-objectid/-/bson-objectid-2.0.4.tgz", + "integrity": "sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001753", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "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.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "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": ">=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==", + "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", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "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, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "license": "MIT", + "peer": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.165", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", + "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", + "license": "ISC" + }, + "node_modules/elysia": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.3.4.tgz", + "integrity": "sha512-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2", + "exact-mirror": "0.1.2", + "fast-decode-uri-component": "^1.0.1" + }, + "optionalDependencies": { + "@sinclair/typebox": "^0.34.33", + "openapi-types": "^12.1.3" + }, + "peerDependencies": { + "@sinclair/typebox": ">= 0.34.0", + "exact-mirror": ">= 0.0.9", + "file-type": ">= 20.0.0", + "openapi-types": ">= 12.0.0", + "typescript": ">= 5.0.0" + } + }, + "node_modules/elysia/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "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, + "license": "MIT", + "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/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.43.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "BSD-2-Clause", + "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/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "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/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.7.tgz", + "integrity": "sha512-0ZxW6guTF/AeKeKi7he93lmgv7Hx7giD1tBrOeVqkqsZGQJd2/kfnL7LdIsr9FT/AtkBK9XeDTov+gxprBqdEg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exact-mirror": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.1.2.tgz", + "integrity": "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw==", + "license": "MIT", + "peerDependencies": { + "@sinclair/typebox": "^0.34.15" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + } + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", + "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "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", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "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/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "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, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "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, + "license": "MIT", + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.6.tgz", + "integrity": "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==", + "license": "MIT" + }, + "node_modules/int53": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/int53/-/int53-0.2.4.tgz", + "integrity": "sha512-a5jlKftS7HUOhkUyYD7j2sJ/ZnvWiNlZS1ldR+g1ifQ+/UuZXIE+YTc/lK1qGj/GwAU5F8Z0e1eVq2t1J5Ob2g==", + "license": "BSD-3-Clause" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isomorphic-dompurify": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.13.0.tgz", + "integrity": "sha512-jVxFnyOiA3fKPkteQjfIogww9T/BIX1Basuwt5D50MB3Sqvki9yBNq96ICLHpbiDY79jc6RC555DeBbTCt6i6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/dompurify": "^3.0.5", + "dompurify": "^3.1.6", + "jsdom": "^24.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isomorphic-dompurify/node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/isomorphic-dompurify/node_modules/jsdom": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/isomorphic-dompurify/node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/isomorphic-dompurify/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "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==", + "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/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "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==", + "license": "MIT", + "peer": 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, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/lzo": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/lzo/-/lzo-0.4.11.tgz", + "integrity": "sha512-apQHNoW2Alg72FMqaC/7pn03I7umdgSVFt2KRkCXXils4Z9u3QBh1uOtl2O5WmZIDLd9g6Lu4lIdOLmiSTFVCQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "~1.2.1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "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", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "license": "Apache-2.0", + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-memory-server": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.1.4.tgz", + "integrity": "sha512-+oKQ/kc3CX+816oPFRtaF0CN4vNcGKNjpOQe4bHo/21A3pMD+lC7Xz1EX5HP7siCX4iCpVchDMmCOFXVQSGkUg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "10.1.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-10.1.4.tgz", + "integrity": "sha512-o8fgY7ZalEd8pGps43fFPr/hkQu1L8i6HFEGbsTfA2zDOW0TopgpswaBCqDr0qD7ptibyPfB5DmC+UlIxbThzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-mutex": "^0.5.0", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^6.9.0", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.7.0", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", + "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/object-stream/-/object-stream-0.0.1.tgz", + "integrity": "sha512-+NPJnRvX9RDMRY9mOWOo/NDppBjbZhXirNNSu2IBnuNboClC9h1ZGHXgHBLDbJMHsxeJDq922aVmG5xs24a/cA==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.111", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz", + "integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parquetjs": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/parquetjs/-/parquetjs-0.11.2.tgz", + "integrity": "sha512-Y6FOc3Oi2AxY4TzJPz7fhICCR8tQNL3p+2xGQoUAMbmlJBR7+JJmMrwuyMjIpDiM7G8Wj/8oqOH4UDUmu4I5ZA==", + "license": "MIT", + "dependencies": { + "brotli": "^1.3.0", + "bson": "^1.0.4", + "int53": "^0.2.4", + "object-stream": "0.0.1", + "snappyjs": "^0.6.0", + "thrift": "^0.11.0", + "varint": "^5.0.0" + }, + "engines": { + "node": ">=7.6" + }, + "optionalDependencies": { + "lzo": "^0.4.0" + } + }, + "node_modules/parquetjs/node_modules/bson": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/peek-readable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pino": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz", + "integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==", + "license": "MIT", + "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": "^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", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz", + "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "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", + "readable-stream": "^4.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==", + "license": "MIT" + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", + "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", + "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "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/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.12.tgz", + "integrity": "sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "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/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT", + "peer": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "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.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "license": "MIT" + }, + "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==", + "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/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "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" + } + ], + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/satori": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.10.14.tgz", + "integrity": "sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-to-react-native": "^3.0.0", + "emoji-regex": "^10.2.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-wasm-web": "^0.3.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satori-html": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz", + "integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==", + "license": "MIT", + "dependencies": { + "ultrahtml": "^1.2.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snappyjs": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/snappyjs/-/snappyjs-0.6.1.tgz", + "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==", + "license": "MIT" + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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==", + "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/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==", + "license": "MIT" + }, + "node_modules/string-width-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", + "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/style-to-object": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.12.tgz", + "integrity": "sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.6" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/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==", + "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/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.33.14", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.14.tgz", + "integrity": "sha512-kRlbhIlMTijbFmVDQFDeKXPLlX1/ovXwV0I162wRqQhRcygaqDIcu1d/Ese3H2uI+yt3uT8E7ndgDthQv5v5BA==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.1.tgz", + "integrity": "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.39", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/svelte/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" + }, + "node_modules/tailwind-scrollbar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz", + "integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "3.x" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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/tailwindcss/node_modules/chokidar/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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/thrift": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/thrift/-/thrift-0.11.0.tgz", + "integrity": "sha512-UpsBhOC45a45TpeHOXE4wwYwL8uD2apbHTbtBvkwtUU4dNwCjC7DpQTjw2Q6eIdfNtw+dKthdwq94uLXTJPfFw==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0", + "q": "^1.5.0", + "ws": ">= 2.2.3" + }, + "engines": { + "node": ">= 4.1.0" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "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/tinypool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", + "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "license": "MIT", + "engines": { + "node": ">=14.0.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==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-icons": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.16.6.tgz", + "integrity": "sha512-jL70sAC7twp4hI/MTfm+vyvTRtHqiEIzf3XOjJz7yzhMEEQnk5Ey5YIXRAU03Mc4BF99ITvvnBzfyRZee86OeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^0.1.1", + "@antfu/utils": "^0.7.6", + "@iconify/utils": "^2.1.9", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "local-pkg": "^0.4.3", + "unplugin": "^1.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@svgr/core": ">=7.0.0", + "@svgx/core": "^1.0.1", + "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", + "vue-template-compiler": "^2.6.12", + "vue-template-es2015-compiler": "^1.9.0" + }, + "peerDependenciesMeta": { + "@svgr/core": { + "optional": true + }, + "@svgx/core": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + }, + "vue-template-es2015-compiler": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.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==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/varint": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", + "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "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.2.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.2.tgz", + "integrity": "sha512-Xj/jovjZvDXOq2FgLXu8NsY4uHUMWtzVmMC2LkCu9HWdr9Qu1Is5sanX3Z4jOFKdohfaWDnEJWp9pRP0vVpAcA==", + "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/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "devOptional": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.2.tgz", + "integrity": "sha512-fyNn/Rp016Bt5qvY0OQvIUCwW2vnaEBLxP42PmKbNIoasSYjML+8xyeADOPvBe+Xfl/ubIw4og7Lt9jflRsCNw==", + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.2", + "@vitest/mocker": "3.2.2", + "@vitest/pretty-format": "^3.2.2", + "@vitest/runner": "3.2.2", + "@vitest/snapshot": "3.2.2", + "@vitest/spy": "3.2.2", + "@vitest/utils": "3.2.2", + "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.0", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.2", + "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.2", + "@vitest/ui": "3.2.2", + "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-browser-svelte": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vitest-browser-svelte/-/vitest-browser-svelte-0.1.0.tgz", + "integrity": "sha512-YB6ZUZZQNqU1T9NzvTEDpwpPv35Ng1NZMPBh81zDrLEdOgROGE6nJb79NWb1Eu/a8VkHifqArpOZfJfALge6xQ==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "^2.1.0 || ^3.0.0-0", + "svelte": ">3.0.0", + "vitest": "^2.1.0 || ^3.0.0-0" + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "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", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/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==", + "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/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==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "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/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yauzl": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-3.3.1.tgz", + "integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^1.0.0" + } + }, + "node_modules/yazl/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "license": "MIT" + }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.55", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.55.tgz", + "integrity": "sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..20aed91ac95adf3c938f5ad2dec420d258888b0c --- /dev/null +++ b/package.json @@ -0,0 +1,113 @@ +{ + "name": "chat-ui", + "version": "0.20.0", + "private": true, + "packageManager": "npm@9.5.0", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "test": "vitest", + "updateLocalEnv": "vite-node --options.transformMode.ssr='/.*/' scripts/updateLocalEnv.ts", + "populate": "vite-node --options.transformMode.ssr='/.*/' scripts/populate.ts", + "config": "vite-node --options.transformMode.ssr='/.*/' scripts/config.ts", + "prepare": "husky" + }, + "devDependencies": { + "@elysiajs/eden": "^1.3.2", + "@faker-js/faker": "^8.4.1", + "@iconify-json/carbon": "^1.1.16", + "@iconify-json/eos-icons": "^1.1.6", + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.21.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/typography": "^0.5.9", + "@types/dompurify": "^3.0.5", + "@types/js-yaml": "^4.0.9", + "@types/jsdom": "^21.1.1", + "@types/katex": "^0.16.7", + "@types/mime-types": "^2.1.4", + "@types/minimist": "^1.2.5", + "@types/node": "^22.1.0", + "@types/parquetjs": "^0.10.3", + "@types/uuid": "^9.0.8", + "@types/yazl": "^3.3.0", + "@typescript-eslint/eslint-plugin": "^6.x", + "@typescript-eslint/parser": "^6.x", + "bson-objectid": "^2.0.4", + "dompurify": "^3.2.4", + "elysia": "^1.3.2", + "eslint": "^8.28.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-svelte": "^2.45.1", + "isomorphic-dompurify": "2.13.0", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "mongodb-memory-server": "^10.1.2", + "playwright": "^1.55.1", + "prettier": "^3.5.3", + "prettier-plugin-svelte": "^3.2.6", + "prettier-plugin-tailwindcss": "^0.6.11", + "sade": "^1.8.1", + "superjson": "^2.2.2", + "svelte": "^5.33.3", + "svelte-check": "^4.0.0", + "tslib": "^2.4.1", + "typescript": "^5.5.0", + "unplugin-icons": "^0.16.1", + "vite": "^6.3.5", + "vite-node": "^3.0.9", + "vitest": "^3.1.4", + "yazl": "^3.3.1" + }, + "type": "module", + "dependencies": { + "@aws-sdk/credential-providers": "^3.925.0", + "@elysiajs/swagger": "^1.3.0", + "@huggingface/hub": "^2.2.0", + "@huggingface/inference": "^4.11.3", + "@iconify-json/bi": "^1.1.21", + "@resvg/resvg-js": "^2.6.2", + "autoprefixer": "^10.4.14", + "aws4": "^1.13.2", + "bits-ui": "^2.14.2", + "date-fns": "^2.29.3", + "dotenv": "^16.5.0", + "file-type": "^21.0.0", + "handlebars": "^4.7.8", + "highlight.js": "^11.7.0", + "husky": "^9.0.11", + "ip-address": "^9.0.5", + "jsdom": "^22.0.0", + "json5": "^2.2.3", + "katex": "^0.16.21", + "lint-staged": "^15.2.7", + "marked": "^12.0.1", + "mime-types": "^2.1.35", + "mongodb": "^5.8.0", + "nanoid": "^5.0.9", + "openai": "^4.44.0", + "openid-client": "^5.4.2", + "parquetjs": "^0.11.2", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + "postcss": "^8.4.31", + "prom-client": "^15.1.3", + "satori": "^0.10.11", + "satori-html": "^0.3.2", + "sharp": "^0.33.4", + "tailwind-scrollbar": "^3.0.0", + "tailwindcss": "^3.4.0", + "undici": "^7.16.0", + "uuid": "^10.0.0", + "vitest-browser-svelte": "^0.1.0", + "zod": "^3.22.3" + }, + "overrides": { + "@reflink/reflink": "file:stub/@reflink/reflink" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..7b75c83aff1c05e0e0e315638e07a22314603d4d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/scripts/config.ts b/scripts/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..2757ee96115b3b8ad2431f135a213e7e38bac0b4 --- /dev/null +++ b/scripts/config.ts @@ -0,0 +1,64 @@ +import sade from "sade"; + +// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them +import { config, ready } from "$lib/server/config"; + +const prog = sade("config"); +await ready; +prog + .command("clear") + .describe("Clear all config keys") + .action(async () => { + console.log("Clearing config..."); + await clear(); + }); + +prog + .command("add ") + .describe("Add a new config key") + .action(async (key: string, value: string) => { + await add(key, value); + }); + +prog + .command("remove ") + .describe("Remove a config key") + .action(async (key: string) => { + console.log(`Removing ${key}`); + await remove(key); + process.exit(0); + }); + +prog + .command("help") + .describe("Show help information") + .action(() => { + prog.help(); + process.exit(0); + }); + +async function clear() { + await config.clear(); + process.exit(0); +} + +async function add(key: string, value: string) { + if (!key || !value) { + console.error("Key and value are required"); + process.exit(1); + } + await config.set(key as keyof typeof config.keysFromEnv, value); + process.exit(0); +} + +async function remove(key: string) { + if (!key) { + console.error("Key is required"); + process.exit(1); + } + await config.delete(key as keyof typeof config.keysFromEnv); + process.exit(0); +} + +// Parse arguments and handle help automatically +prog.parse(process.argv); diff --git a/scripts/populate.ts b/scripts/populate.ts new file mode 100755 index 0000000000000000000000000000000000000000..3590a5fd1b52b0483d24be49c3bece37b5ef863d --- /dev/null +++ b/scripts/populate.ts @@ -0,0 +1,288 @@ +import readline from "readline"; +import minimist from "minimist"; + +// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them +import { env } from "$env/dynamic/private"; + +import { faker } from "@faker-js/faker"; +import { ObjectId } from "mongodb"; + +// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them +import { ready } from "$lib/server/config"; +import { collections } from "$lib/server/database.ts"; +import { models } from "../src/lib/server/models.ts"; +import type { User } from "../src/lib/types/User"; +import type { Assistant } from "../src/lib/types/Assistant"; +import type { Conversation } from "../src/lib/types/Conversation"; +import type { Settings } from "../src/lib/types/Settings"; +import { Message } from "../src/lib/types/Message.ts"; + +import { addChildren } from "../src/lib/utils/tree/addChildren.ts"; +import { generateSearchTokens } from "../src/lib/utils/searchTokens.ts"; +import { ReviewStatus } from "../src/lib/types/Review.ts"; +import fs from "fs"; +import path from "path"; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +await ready; + +rl.on("close", function () { + process.exit(0); +}); + +const samples = fs.readFileSync(path.join(__dirname, "samples.txt"), "utf8").split("\n---\n"); + +const possibleFlags = ["reset", "all", "users", "settings", "assistants", "conversations"]; +const argv = minimist(process.argv.slice(2)); +const flags = argv["_"].filter((flag) => possibleFlags.includes(flag)); + +async function generateMessages(preprompt?: string): Promise { + const isLinear = faker.datatype.boolean(0.5); + const isInterrupted = faker.datatype.boolean(0.05); + + const messages: Message[] = []; + + messages.push({ + id: crypto.randomUUID(), + from: "system", + content: preprompt ?? "", + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + }); + + let isUser = true; + let lastId = messages[0].id; + if (isLinear) { + const convLength = faker.number.int({ min: 1, max: 25 }) * 2; // must always be even + + for (let i = 0; i < convLength; i++) { + lastId = addChildren( + { + messages, + rootMessageId: messages[0].id, + }, + { + from: isUser ? "user" : "assistant", + content: + faker.lorem.sentence({ + min: 10, + max: isUser ? 50 : 200, + }) + + (!isUser && Math.random() < 0.1 + ? "\n```\n" + faker.helpers.arrayElement(samples) + "\n```\n" + : ""), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + interrupted: !isUser && i === convLength - 1 && isInterrupted, + }, + lastId + ); + isUser = !isUser; + } + } else { + const convLength = faker.number.int({ min: 2, max: 200 }); + + for (let i = 0; i < convLength; i++) { + addChildren( + { + messages, + rootMessageId: messages[0].id, + }, + { + from: isUser ? "user" : "assistant", + content: + faker.lorem.sentence({ + min: 10, + max: isUser ? 50 : 200, + }) + + (!isUser && Math.random() < 0.1 + ? "\n```\n" + faker.helpers.arrayElement(samples) + "\n```\n" + : ""), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + interrupted: !isUser && i === convLength - 1 && isInterrupted, + }, + faker.helpers.arrayElement([ + messages[0].id, + ...messages.filter((m) => m.from === (isUser ? "assistant" : "user")).map((m) => m.id), + ]) + ); + + isUser = !isUser; + } + } + return messages; +} + +async function seed() { + console.log("Seeding..."); + const modelIds = models.map((model) => model.id); + + if (flags.includes("reset")) { + console.log("Starting reset of DB"); + await collections.users.deleteMany({}); + await collections.settings.deleteMany({}); + await collections.assistants.deleteMany({}); + await collections.conversations.deleteMany({}); + await collections.migrationResults.deleteMany({}); + await collections.semaphores.deleteMany({}); + console.log("Reset done"); + } + + if (flags.includes("users") || flags.includes("all")) { + console.log("Creating 100 new users"); + const newUsers: User[] = Array.from({ length: 100 }, () => ({ + _id: new ObjectId(), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + username: faker.internet.userName(), + name: faker.person.fullName(), + hfUserId: faker.string.alphanumeric(24), + avatarUrl: faker.image.avatar(), + })); + + await collections.users.insertMany(newUsers); + console.log("Done creating users."); + } + + const users = await collections.users.find().toArray(); + if (flags.includes("settings") || flags.includes("all")) { + console.log("Updating settings for all users"); + users.forEach(async (user) => { + const settings: Settings = { + userId: user._id, + shareConversationsWithModelAuthors: faker.datatype.boolean(0.25), + hideEmojiOnSidebar: faker.datatype.boolean(0.25), + activeModel: faker.helpers.arrayElement(modelIds), + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + disableStream: faker.datatype.boolean(0.25), + directPaste: faker.datatype.boolean(0.25), + hidePromptExamples: {}, + customPrompts: {}, + assistants: [], + }; + await collections.settings.updateOne( + { userId: user._id }, + { $set: { ...settings } }, + { upsert: true } + ); + }); + console.log("Done updating settings."); + } + + if (flags.includes("assistants") || flags.includes("all")) { + console.log("Creating assistants for all users"); + await Promise.all( + users.map(async (user) => { + const name = faker.animal.insect(); + const assistants = faker.helpers.multiple( + () => ({ + _id: new ObjectId(), + name, + createdById: user._id, + createdByName: user.username, + createdAt: faker.date.recent({ days: 30 }), + updatedAt: faker.date.recent({ days: 30 }), + userCount: faker.number.int({ min: 1, max: 100000 }), + review: faker.helpers.enumValue(ReviewStatus), + modelId: faker.helpers.arrayElement(modelIds), + description: faker.lorem.sentence(), + preprompt: faker.hacker.phrase(), + exampleInputs: faker.helpers.multiple(() => faker.lorem.sentence(), { + count: faker.number.int({ min: 0, max: 4 }), + }), + searchTokens: generateSearchTokens(name), + last24HoursCount: faker.number.int({ min: 0, max: 1000 }), + }), + { count: faker.number.int({ min: 3, max: 10 }) } + ); + await collections.assistants.insertMany(assistants); + await collections.settings.updateOne( + { userId: user._id }, + { $set: { assistants: assistants.map((a) => a._id.toString()) } }, + { upsert: true } + ); + }) + ); + console.log("Done creating assistants."); + } + + if (flags.includes("conversations") || flags.includes("all")) { + console.log("Creating conversations for all users"); + await Promise.all( + users.map(async (user) => { + const conversations = faker.helpers.multiple( + async () => { + const settings = await collections.settings.findOne({ userId: user._id }); + + const assistantId = + settings?.assistants && settings.assistants.length > 0 && faker.datatype.boolean(0.1) + ? faker.helpers.arrayElement(settings.assistants) + : undefined; + + const preprompt = + (assistantId + ? await collections.assistants + .findOne({ _id: assistantId }) + .then((assistant: Assistant) => assistant?.preprompt ?? "") + : faker.helpers.maybe(() => faker.hacker.phrase(), { probability: 0.5 })) ?? ""; + + const messages = await generateMessages(preprompt); + + const conv = { + _id: new ObjectId(), + userId: user._id, + assistantId, + preprompt, + createdAt: faker.date.recent({ days: 145 }), + updatedAt: faker.date.recent({ days: 145 }), + model: faker.helpers.arrayElement(modelIds), + title: faker.internet.emoji() + " " + faker.hacker.phrase(), + // embeddings removed in this build + messages, + rootMessageId: messages[0].id, + } satisfies Conversation; + + return conv; + }, + { count: faker.number.int({ min: 10, max: 200 }) } + ); + + await collections.conversations.insertMany(await Promise.all(conversations)); + }) + ); + console.log("Done creating conversations."); + } +} + +// run seed +(async () => { + try { + rl.question( + "You're about to run a seeding script on the following MONGODB_URL: \x1b[31m" + + env.MONGODB_URL + + "\x1b[0m\n\n With the following flags: \x1b[31m" + + flags.join("\x1b[0m , \x1b[31m") + + "\x1b[0m\n \n\n Are you sure you want to continue? (yes/no): ", + async (confirm) => { + if (confirm !== "yes") { + console.log("Not 'yes', exiting."); + rl.close(); + process.exit(0); + } + console.log("Starting seeding..."); + await seed(); + console.log("Seeding done."); + rl.close(); + } + ); + } catch (e) { + console.error(e); + process.exit(1); + } +})(); diff --git a/scripts/samples.txt b/scripts/samples.txt new file mode 100644 index 0000000000000000000000000000000000000000..acca18ac4ee3bca49e6a4c59a380dc5b635703a9 --- /dev/null +++ b/scripts/samples.txt @@ -0,0 +1,194 @@ +import { Observable, of, from, interval, throwError } from 'rxjs'; +import { map, filter, catchError, switchMap, take, tap } from 'rxjs/operators'; + +// Mock function to fetch stock prices (simulates API call) +const fetchStockPrice = (ticker: string): Observable => { + return new Observable((observer) => { + const intervalId = setInterval(() => { + if (Math.random() < 0.1) { // Simulating an error 10% of the time + observer.error(`Error fetching stock price for ${ticker}`); + } else { + const price = parseFloat((Math.random() * 1000).toFixed(2)); + observer.next(price); + } + }, 1000); + + return () => { + clearInterval(intervalId); + console.log(`Stopped fetching prices for ${ticker}`); + }; + }); +}; + +// Example usage: Tracking stock price updates +const stockTicker = 'AAPL'; +const stockPrice$ = fetchStockPrice(stockTicker).pipe( + map(price => ({ ticker: stockTicker, price })), // Transform data + filter(data => data.price > 500), // Only keep prices above 500 + tap(data => console.log(`Price update:`, data)), // Side effect: Logging + catchError(err => { + console.error(err); + return of({ ticker: stockTicker, price: null }); // Fallback observable + }) +); + +// Subscribe to the stock price updates +const subscription = stockPrice$.subscribe({ + next: data => console.log(`Subscriber received:`, data), + error: err => console.error(`Subscription error:`, err), + complete: () => console.log('Stream complete'), +}); + +// Automatically unsubscribe after 10 seconds +setTimeout(() => { + subscription.unsubscribe(); + console.log('Unsubscribed from stock price updates.'); +}, 10000); +--- +class EnforceAttrsMeta(type): + """ + Metaclass that enforces the presence of specific attributes in a class + and automatically decorates methods with a logging wrapper. + """ + + required_attributes = ['name', 'version'] + + def __new__(cls, name, bases, class_dict): + """ + Create a new class with enforced attributes and method logging. + + :param name: Name of the class being created. + :param bases: Tuple of base classes. + :param class_dict: Dictionary of attributes and methods of the class. + :return: Newly created class object. + """ + # Ensure required attributes exist + for attr in cls.required_attributes: + if attr not in class_dict: + raise TypeError(f"Class '{name}' is missing required attribute '{attr}'") + + # Wrap all methods in a logging decorator + for key, value in class_dict.items(): + if callable(value): # Check if it's a method + class_dict[key] = cls.log_calls(value) + + return super().__new__(cls, name, bases, class_dict) + + @staticmethod + def log_calls(func): + """ + Decorator that logs method calls and arguments. + + :param func: Function to be wrapped. + :return: Wrapped function with logging. + """ + def wrapper(*args, **kwargs): + print(f"Calling {func.__name__} with args={args} kwargs={kwargs}") + result = func(*args, **kwargs) + print(f"{func.__name__} returned {result}") + return result + return wrapper + + +class PluginBase(metaclass=EnforceAttrsMeta): + """ + Base class for plugins that enforces required attributes and logging. + """ + name = "BasePlugin" + version = "1.0" + + def run(self, data): + """ + Process the input data. + + :param data: The data to be processed. + :return: Processed result. + """ + return f"Processed {data}" + + +class CustomPlugin(PluginBase): + """ + Custom plugin that extends PluginBase and adheres to enforced rules. + """ + name = "CustomPlugin" + version = "2.0" + + def run(self, data): + """ + Custom processing logic. + + :param data: The data to process. + :return: Modified data. + """ + return f"Custom processing of {data}" + + +# Uncommenting the following class definition will raise a TypeError +# because 'version' attribute is missing. +# class InvalidPlugin(PluginBase): +# name = "InvalidPlugin" + + +if __name__ == "__main__": + # Instantiate and use the plugin + plugin = CustomPlugin() + print(plugin.run("example data")) +--- + + + + + + Click the Box Game + + + +

Click the Box!

+

Score: 0

+
+
+
+ + + diff --git a/scripts/setups/vitest-setup-client.ts b/scripts/setups/vitest-setup-client.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/setups/vitest-setup-server.ts b/scripts/setups/vitest-setup-server.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a486366c3a62526d7a02b4e5a0f9dd4c6cb6e46 --- /dev/null +++ b/scripts/setups/vitest-setup-server.ts @@ -0,0 +1,49 @@ +import { vi, afterAll } from "vitest"; +import dotenv from "dotenv"; +import { resolve } from "path"; +import fs from "fs"; +import { MongoMemoryServer } from "mongodb-memory-server"; + +let mongoServer: MongoMemoryServer; +// Load the .env file +const envPath = resolve(__dirname, "../../.env"); +dotenv.config({ path: envPath }); + +// Read the .env file content +const envContent = fs.readFileSync(envPath, "utf-8"); + +// Parse the .env content +const envVars = dotenv.parse(envContent); + +// Separate public and private variables +const publicEnv = {}; +const privateEnv = {}; + +for (const [key, value] of Object.entries(envVars)) { + if (key.startsWith("PUBLIC_")) { + publicEnv[key] = value; + } else { + privateEnv[key] = value; + } +} + +vi.mock("$env/dynamic/public", () => ({ + env: publicEnv, +})); + +vi.mock("$env/dynamic/private", async () => { + mongoServer = await MongoMemoryServer.create(); + + return { + env: { + ...privateEnv, + MONGODB_URL: mongoServer.getUri(), + }, + }; +}); + +afterAll(async () => { + if (mongoServer) { + await mongoServer.stop(); + } +}); diff --git a/scripts/updateLocalEnv.ts b/scripts/updateLocalEnv.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc609d6a2e4b5a4ae20d5c68aa02929784e6a35e --- /dev/null +++ b/scripts/updateLocalEnv.ts @@ -0,0 +1,48 @@ +import fs from "fs"; +import yaml from "js-yaml"; + +const file = fs.readFileSync("chart/env/prod.yaml", "utf8"); + +// have to do a weird stringify/parse because of some node error +const prod = JSON.parse(JSON.stringify(yaml.load(file))); +const vars = prod.envVars as Record; + +let PUBLIC_CONFIG = ""; + +Object.entries(vars) + // filter keys used in prod with the proxy + .filter( + ([key]) => + ![ + "XFF_DEPTH", + "ADDRESS_HEADER", + "APP_BASE", + "PUBLIC_ORIGIN", + "PUBLIC_SHARE_PREFIX", + "ADMIN_CLI_LOGIN", + ].includes(key) + ) + .forEach(([key, value]) => { + PUBLIC_CONFIG += `${key}=\`${value}\`\n`; + }); + +const SECRET_CONFIG = + (fs.existsSync(".env.SECRET_CONFIG") + ? fs.readFileSync(".env.SECRET_CONFIG", "utf8") + : process.env.SECRET_CONFIG) ?? ""; + +// Prepend the content of the env variable SECRET_CONFIG +let full_config = `${PUBLIC_CONFIG}\n${SECRET_CONFIG}`; + +// replace the internal proxy url with the public endpoint +full_config = full_config.replaceAll( + "https://internal.api-inference.huggingface.co", + "https://router.huggingface.co/hf-inference" +); + +full_config = full_config.replaceAll("COOKIE_SECURE=`true`", "COOKIE_SECURE=`false`"); +full_config = full_config.replaceAll("LOG_LEVEL=`debug`", "LOG_LEVEL=`info`"); +full_config = full_config.replaceAll("NODE_ENV=`prod`", "NODE_ENV=`development`"); + +// Write full_config to .env.local +fs.writeFileSync(".env.local", full_config); diff --git a/server.log b/server.log new file mode 100644 index 0000000000000000000000000000000000000000..9a27281e85b88b6d68f3e9e36d327fcbe1d14e4d --- /dev/null +++ b/server.log @@ -0,0 +1,2 @@ +/Users/vm/.venv/bin/python3: No module named uvicorn +/Users/vm/.venv/bin/python3: No module named uvicorn diff --git a/src/ambient.d.ts b/src/ambient.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..406da97f6882cf6aaa3c594e7ac5afb1a96e7fe3 --- /dev/null +++ b/src/ambient.d.ts @@ -0,0 +1,7 @@ +declare module "*.ttf" { + const value: ArrayBuffer; + export default value; +} + +// Legacy helpers removed: web search support is deprecated, so we intentionally +// avoid leaking those shapes into the global ambient types. diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..f25a2e09315f98d28e7e8bd4bb9b95d63367479f --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,27 @@ +/// +/// + +import type { User } from "$lib/types/User"; + +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + interface Locals { + sessionId: string; + user?: User; + isAdmin: boolean; + token?: string; + } + + interface Error { + message: string; + errorId?: ReturnType; + } + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000000000000000000000000000000000000..43f0ca64088ec2bd4f7035c01fcc4d8122ffe020 --- /dev/null +++ b/src/app.html @@ -0,0 +1,50 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + + + + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a7ac2f227fe020b84f28100eda62f0e4eb45826 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,305 @@ +import { config, ready } from "$lib/server/config"; +import type { Handle, HandleServerError, ServerInit, HandleFetch } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import { base } from "$app/paths"; +import { + authenticateRequest, + loginEnabled, + refreshSessionCookie, + triggerOauthFlow, +} from "$lib/server/auth"; +import { ERROR_MESSAGES } from "$lib/stores/errors"; +import { addWeeks } from "date-fns"; +import { checkAndRunMigrations } from "$lib/migrations/migrations"; +import { building, dev } from "$app/environment"; +import { logger } from "$lib/server/logger"; +import { AbortedGenerations } from "$lib/server/abortedGenerations"; +import { initExitHandler } from "$lib/server/exitHandler"; +import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats"; +import { adminTokenManager } from "$lib/server/adminToken"; +import { isHostLocalhost } from "$lib/server/isURLLocal"; +import { MetricsServer } from "$lib/server/metrics"; + +export const init: ServerInit = async () => { + // Wait for config to be fully loaded + await ready; + + // TODO: move this code on a started server hook, instead of using a "building" flag + if (!building) { + // Ensure legacy env expected by some libs: map OPENAI_API_KEY -> HF_TOKEN if absent + const canonicalToken = config.OPENAI_API_KEY || config.HF_TOKEN; + if (canonicalToken) { + process.env.HF_TOKEN ??= canonicalToken; + } + + // Warn if legacy-only var is used + if (!config.OPENAI_API_KEY && config.HF_TOKEN) { + logger.warn( + "HF_TOKEN is deprecated in favor of OPENAI_API_KEY. Please migrate to OPENAI_API_KEY." + ); + } + + logger.info("Starting server..."); + initExitHandler(); + + if (config.METRICS_ENABLED === "true") { + MetricsServer.getInstance(); + } + + checkAndRunMigrations(); + refreshConversationStats(); + + // Init AbortedGenerations refresh process + AbortedGenerations.getInstance(); + + adminTokenManager.displayToken(); + + if (config.EXPOSE_API) { + logger.warn( + "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work." + ); + } + } +}; + +export const handleError: HandleServerError = async ({ error, event, status, message }) => { + // handle 404 + + if (building) { + throw error; + } + + if (event.route.id === null) { + return { + message: `Page ${event.url.pathname} not found`, + }; + } + + const errorId = crypto.randomUUID(); + + logger.error({ + locals: event.locals, + url: event.request.url, + params: event.params, + request: event.request, + message, + error, + errorId, + status, + stack: error instanceof Error ? error.stack : undefined, + }); + + return { + message: "An error occurred", + errorId, + }; +}; + +export const handle: Handle = async ({ event, resolve }) => { + await ready.then(() => { + config.checkForUpdates(); + }); + + logger.debug({ + locals: event.locals, + url: event.url.pathname, + params: event.params, + request: event.request, + }); + + function errorResponse(status: number, message: string) { + const sendJson = + event.request.headers.get("accept")?.includes("application/json") || + event.request.headers.get("content-type")?.includes("application/json"); + return new Response(sendJson ? JSON.stringify({ error: message }) : message, { + status, + headers: { + "content-type": sendJson ? "application/json" : "text/plain", + }, + }); + } + + if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) { + const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET; + + if (!ADMIN_SECRET) { + return errorResponse(500, "Admin API is not configured"); + } + + if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) { + return errorResponse(401, "Unauthorized"); + } + } + + const auth = await authenticateRequest( + { type: "svelte", value: event.request.headers }, + { type: "svelte", value: event.cookies }, + event.url + ); + + event.locals.sessionId = auth.sessionId; + + if (loginEnabled && !auth.user) { + if (config.AUTOMATIC_LOGIN === "true") { + // AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages) + if ( + !event.url.pathname.startsWith(`${base}/login`) && + !event.url.pathname.startsWith(`${base}/healthcheck`) + ) { + // To get the same CSRF token after callback + refreshSessionCookie(event.cookies, auth.secretSessionId); + return await triggerOauthFlow({ + request: event.request, + url: event.url, + locals: event.locals, + }); + } + } else { + // Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails) + if ( + event.url.pathname !== `${base}/` && + event.url.pathname !== `${base}` && + !event.url.pathname.startsWith(`${base}/login`) && + !event.url.pathname.startsWith(`${base}/login/callback`) && + !event.url.pathname.startsWith(`${base}/healthcheck`) && + !event.url.pathname.startsWith(`${base}/r/`) && + !event.url.pathname.startsWith(`${base}/conversation/`) && + !event.url.pathname.startsWith(`${base}/models/`) && + !event.url.pathname.startsWith(`${base}/api`) + ) { + refreshSessionCookie(event.cookies, auth.secretSessionId); + return triggerOauthFlow({ request: event.request, url: event.url, locals: event.locals }); + } + } + } + + event.locals.user = auth.user || undefined; + event.locals.token = auth.token; + + event.locals.isAdmin = + event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId); + + // CSRF protection + const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? ""; + /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */ + const nativeFormContentTypes = [ + "multipart/form-data", + "application/x-www-form-urlencoded", + "text/plain", + ]; + + if (event.request.method === "POST") { + if (nativeFormContentTypes.includes(requestContentType)) { + const origin = event.request.headers.get("origin"); + + if (!origin) { + return errorResponse(403, "Non-JSON form requests need to have an origin"); + } + + const validOrigins = [ + new URL(event.request.url).host, + ...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []), + ]; + + if (!validOrigins.includes(new URL(origin).host)) { + return errorResponse(403, "Invalid referer for POST request"); + } + } + } + + if ( + event.request.method === "POST" || + event.url.pathname.startsWith(`${base}/login`) || + event.url.pathname.startsWith(`${base}/login/callback`) + ) { + // if the request is a POST request or login-related we refresh the cookie + refreshSessionCookie(event.cookies, auth.secretSessionId); + + await collections.sessions.updateOne( + { sessionId: auth.sessionId }, + { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } } + ); + } + + if ( + loginEnabled && + !event.locals.user && + !event.url.pathname.startsWith(`${base}/login`) && + !event.url.pathname.startsWith(`${base}/admin`) && + !event.url.pathname.startsWith(`${base}/settings`) && + !["GET", "OPTIONS", "HEAD"].includes(event.request.method) + ) { + return errorResponse(401, ERROR_MESSAGES.authOnly); + } + + let replaced = false; + + const response = await resolve(event, { + transformPageChunk: (chunk) => { + // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template + if (replaced || !chunk.html.includes("%gaId%")) { + return chunk.html; + } + replaced = true; + + return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID); + }, + filterSerializedResponseHeaders: (header) => { + return header.includes("content-type"); + }, + }); + + // Add CSP header to disallow framing if ALLOW_IFRAME is not "true" + if (config.ALLOW_IFRAME !== "true") { + response.headers.append("Content-Security-Policy", "frame-ancestors 'none';"); + } + + if ( + event.url.pathname.startsWith(`${base}/login/callback`) || + event.url.pathname.startsWith(`${base}/login`) + ) { + response.headers.append("Cache-Control", "no-store"); + } + + if (event.url.pathname.startsWith(`${base}/api/`)) { + // get origin from the request + const requestOrigin = event.request.headers.get("origin"); + + // get origin from the config if its defined + let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined; + + if ( + dev || // if we're in dev mode + !requestOrigin || // or the origin is null (SSR) + isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost + ) { + allowedOrigin = "*"; // allow all origins + } else if (allowedOrigin === requestOrigin) { + allowedOrigin = requestOrigin; // echo back the caller + } + + if (allowedOrigin) { + response.headers.set("Access-Control-Allow-Origin", allowedOrigin); + response.headers.set( + "Access-Control-Allow-Methods", + "GET, POST, PUT, PATCH, DELETE, OPTIONS" + ); + response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization"); + } + } + return response; +}; + +export const handleFetch: HandleFetch = async ({ event, request, fetch }) => { + if (isHostLocalhost(new URL(request.url).hostname)) { + const cookieHeader = event.request.headers.get("cookie"); + if (cookieHeader) { + const headers = new Headers(request.headers); + headers.set("cookie", cookieHeader); + + return fetch(new Request(request, { headers })); + } + } + + return fetch(request); +}; diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac3631a56b2fc022e917c63c3e5aba95b29bb6ff --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,6 @@ +import { publicConfigTransporter } from "$lib/utils/PublicConfig.svelte"; +import type { Transport } from "@sveltejs/kit"; + +export const transport: Transport = { + PublicConfig: publicConfigTransporter, +}; diff --git a/src/lib/APIClient.ts b/src/lib/APIClient.ts new file mode 100644 index 0000000000000000000000000000000000000000..8bec8e5213fca5bc664bb5a68f6e909fb351af31 --- /dev/null +++ b/src/lib/APIClient.ts @@ -0,0 +1,51 @@ +import type { App } from "$api"; +import { base } from "$app/paths"; +import { treaty, type Treaty } from "@elysiajs/eden"; +import { browser } from "$app/environment"; +import superjson from "superjson"; +import ObjectId from "bson-objectid"; + +superjson.registerCustom( + { + isApplicable: (value): value is ObjectId => { + if (typeof value !== "string" && ObjectId.isValid(value)) { + const str = value.toString(); + return /^[0-9a-fA-F]{24}$/.test(str); + } + return false; + }, + serialize: (value) => value.toString(), + deserialize: (value) => new ObjectId(value), + }, + "ObjectId" +); + +export function useAPIClient({ + fetch, + origin, +}: { + fetch?: Treaty.Config["fetcher"]; + origin?: string; +} = {}) { + // On the server, use the current request origin when available to avoid + // incorrect port guessing and ensure cookies are forwarded properly. + // Fall back to a sane default in dev if origin is missing. + const url = browser + ? `${window.location.origin}${base}/api/v2` + : `${origin ?? `http://localhost:5173`}${base}/api/v2`; + + const app = treaty(url, { fetcher: fetch }); + return app; +} + +export function handleResponse>( + response: Treaty.TreatyResponse +): T[200] { + if (response.error) { + throw new Error(JSON.stringify(response.error)); + } + + return superjson.parse( + typeof response.data === "string" ? response.data : JSON.stringify(response.data) + ) as T[200]; +} diff --git a/src/lib/actions/clickOutside.ts b/src/lib/actions/clickOutside.ts new file mode 100644 index 0000000000000000000000000000000000000000..6aa146932fea03a06fa3c654c58460d33c389850 --- /dev/null +++ b/src/lib/actions/clickOutside.ts @@ -0,0 +1,18 @@ +export function clickOutside(element: HTMLElement, callbackFunction: () => void) { + function onClick(event: MouseEvent) { + if (!element.contains(event.target as Node)) { + callbackFunction(); + } + } + + document.body.addEventListener("click", onClick); + + return { + update(newCallbackFunction: () => void) { + callbackFunction = newCallbackFunction; + }, + destroy() { + document.body.removeEventListener("click", onClick); + }, + }; +} diff --git a/src/lib/actions/snapScrollToBottom.ts b/src/lib/actions/snapScrollToBottom.ts new file mode 100644 index 0000000000000000000000000000000000000000..986067aba51821ac8b902c94965baeac0ffbbd3a --- /dev/null +++ b/src/lib/actions/snapScrollToBottom.ts @@ -0,0 +1,87 @@ +import { navigating } from "$app/state"; +import { tick } from "svelte"; + +const detachedOffset = 10; + +const waitForAnimationFrame = () => + typeof requestAnimationFrame === "function" + ? new Promise((resolve) => { + requestAnimationFrame(() => resolve()); + }) + : Promise.resolve(); + +/** + * @param node element to snap scroll to bottom + * @param dependency pass in a dependency to update scroll on changes. + */ +export const snapScrollToBottom = (node: HTMLElement, dependency: unknown) => { + let prevScrollValue = node.scrollTop; + let isDetached = false; + let resizeObserver: ResizeObserver | undefined; + + const scrollToBottom = () => { + node.scrollTo({ top: node.scrollHeight }); + }; + + const distanceFromBottom = () => node.scrollHeight - node.scrollTop - node.clientHeight; + + async function updateScroll(_options: { force?: boolean } = {}) { + const options = { force: false, ..._options }; + const { force } = options; + + if (!force && isDetached && !navigating.to) return; + + // wait for the next tick to ensure that the DOM is updated + await tick(); + scrollToBottom(); + + // ensure we settle after late layout shifts (e.g. markdown/image renders) + if (typeof requestAnimationFrame === "function") { + await waitForAnimationFrame(); + scrollToBottom(); + await waitForAnimationFrame(); + scrollToBottom(); + } + } + + const handleScroll = () => { + // if user scrolled up, we detach + if (node.scrollTop < prevScrollValue) { + isDetached = true; + } + + const atBottom = distanceFromBottom() <= detachedOffset; + if (atBottom) { + const wasDetached = isDetached; + isDetached = false; + if (wasDetached) { + void updateScroll({ force: true }); + } + } + + prevScrollValue = node.scrollTop; + }; + + node.addEventListener("scroll", handleScroll); + + if (typeof ResizeObserver !== "undefined") { + const target = node.firstElementChild ?? node; + resizeObserver = new ResizeObserver(() => { + if (isDetached && !navigating.to) return; + scrollToBottom(); + }); + resizeObserver.observe(target); + } + + if (dependency) { + void updateScroll({ force: true }); + } + + return { + update: updateScroll, + destroy: () => { + node.removeEventListener("scroll", handleScroll); + resizeObserver?.disconnect(); + }, + }; +}; diff --git a/src/lib/buildPrompt.ts b/src/lib/buildPrompt.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d7458db0c6b66e1ccd5a14e67dbc1e94339fb2a --- /dev/null +++ b/src/lib/buildPrompt.ts @@ -0,0 +1,33 @@ +import type { EndpointParameters } from "./server/endpoints/endpoints"; +import type { BackendModel } from "./server/models"; + +type buildPromptOptions = Pick & { + model: BackendModel; +}; + +export async function buildPrompt({ + messages, + model, + preprompt, +}: buildPromptOptions): Promise { + const filteredMessages = messages; + + if (filteredMessages[0].from === "system" && preprompt) { + filteredMessages[0].content = preprompt; + } + + const prompt = model + .chatPromptRender({ + messages: filteredMessages.map((m) => ({ + ...m, + role: m.from, + })), + preprompt, + }) + // Not super precise, but it's truncated in the model's backend anyway + .split(" ") + .slice(-(model.parameters?.truncate ?? 0)) + .join(" "); + + return prompt; +} diff --git a/src/lib/components/AnnouncementBanner.svelte b/src/lib/components/AnnouncementBanner.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f1b064049539b8b049477ece06496428a870e2ee --- /dev/null +++ b/src/lib/components/AnnouncementBanner.svelte @@ -0,0 +1,20 @@ + + +
+ New + {title} +
+ {@render children?.()} +
+
diff --git a/src/lib/components/BackgroundGenerationPoller.svelte b/src/lib/components/BackgroundGenerationPoller.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9524e30cebc38fb40bc58b6280dc74ca5fff9d4a --- /dev/null +++ b/src/lib/components/BackgroundGenerationPoller.svelte @@ -0,0 +1,174 @@ + diff --git a/src/lib/components/CodeBlock.svelte b/src/lib/components/CodeBlock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4d275d0b1c572023cb890c49133d3ebb37f6acc6 --- /dev/null +++ b/src/lib/components/CodeBlock.svelte @@ -0,0 +1,73 @@ + + +
+
+
+ {#if showPreview} + + {/if} + +
+
+
{@html DOMPurify.sanitize(code)}
+ + {#if previewOpen} + (previewOpen = false)} /> + {/if} +
diff --git a/src/lib/components/CopyToClipBoardBtn.svelte b/src/lib/components/CopyToClipBoardBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ecd6e18e31a6e058c27992d947c100f93f360ef2 --- /dev/null +++ b/src/lib/components/CopyToClipBoardBtn.svelte @@ -0,0 +1,90 @@ + + + diff --git a/src/lib/components/EditConversationModal.svelte b/src/lib/components/EditConversationModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..54badb0f34e73defcc44cdc25dc6c06416222016 --- /dev/null +++ b/src/lib/components/EditConversationModal.svelte @@ -0,0 +1,100 @@ + + +{#if open} + +
{ + e.preventDefault(); + save(); + }} + > +
+

Rename conversation

+ +
+ +
+ + (newTitle = (e.currentTarget as HTMLInputElement).value)} + class="w-full rounded-xl border border-gray-200 bg-white px-3 py-2 text-[15px] text-gray-800 outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-500 dark:focus:ring-gray-700" + placeholder="Enter a title" + /> +
+ +
+ + +
+
+
+{/if} diff --git a/src/lib/components/ExpandNavigation.svelte b/src/lib/components/ExpandNavigation.svelte new file mode 100644 index 0000000000000000000000000000000000000000..45aa691f1e00791538b9631242b1de379bcfaa1a --- /dev/null +++ b/src/lib/components/ExpandNavigation.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/HoverTooltip.svelte b/src/lib/components/HoverTooltip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9fe990defdb422287e3cb27d9f2fa31d411e3d78 --- /dev/null +++ b/src/lib/components/HoverTooltip.svelte @@ -0,0 +1,44 @@ + + +
+ {@render children?.()} + + +
diff --git a/src/lib/components/HtmlPreviewModal.svelte b/src/lib/components/HtmlPreviewModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3f3e0299314aa823a7c6dba065a00210aae92317 --- /dev/null +++ b/src/lib/components/HtmlPreviewModal.svelte @@ -0,0 +1,156 @@ + + + + + onclose?.()}> +
+
+ + + {#if errors.length > 0} + + {/if} +
+
+
diff --git a/src/lib/components/InfiniteScroll.svelte b/src/lib/components/InfiniteScroll.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ca8926cf11b95c81bf2902cdeb6d219092939254 --- /dev/null +++ b/src/lib/components/InfiniteScroll.svelte @@ -0,0 +1,50 @@ + + +
diff --git a/src/lib/components/MobileNav.svelte b/src/lib/components/MobileNav.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4d78cfda93caf983e2e964b984e435bd9c5c88b3 --- /dev/null +++ b/src/lib/components/MobileNav.svelte @@ -0,0 +1,144 @@ + + + + + + + +{#if isOpen} + +{/if} + + diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a3b357dfede0d0d754876741d57a15e317b7e8c6 --- /dev/null +++ b/src/lib/components/Modal.svelte @@ -0,0 +1,113 @@ + + + +
{ + e.stopPropagation(); + handleBackdropClick(e); + }} + transition:fade|local={{ easing: cubicOut, duration: 300 }} + class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50" + > + {#if disableFly} + + {:else} + + {/if} +
+
diff --git a/src/lib/components/ModelCardMetadata.svelte b/src/lib/components/ModelCardMetadata.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e626a442c0a634ddd3d7134eebff37edaae79a34 --- /dev/null +++ b/src/lib/components/ModelCardMetadata.svelte @@ -0,0 +1,71 @@ + + +
+ + Model +
 page
+ {#if model.datasetName || model.datasetUrl} + + Dataset +
 page
+ {/if} + {#if model.hasInferenceAPI} + + API + + {/if} + {#if model.websiteUrl} + + {#if model.name.startsWith("meta-llama/Meta-Llama")} + + Built with Llama + {:else} + + Website + {/if} + + {/if} +
diff --git a/src/lib/components/NavConversationItem.svelte b/src/lib/components/NavConversationItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ed830b4fef3221f92f2ceb6914ff1980d6edf4e4 --- /dev/null +++ b/src/lib/components/NavConversationItem.svelte @@ -0,0 +1,121 @@ + + + { + confirmDelete = false; + }} + href="{base}/conversation/{conv.id}" + class="group flex h-[2.15rem] flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 max-sm:h-10 + {conv.id === page.params.id ? 'bg-gray-100 dark:bg-gray-700' : ''}" +> +
+ + {#if confirmDelete} + Delete? + {/if} + {conv.title} + +
+ + {#if !readOnly} + {#if confirmDelete} + + + {:else} + + + + {/if} + {/if} +
+ + +{#if renameOpen} + (renameOpen = false)} + onsave={(payload) => { + renameOpen = false; + oneditConversationTitle?.({ id: conv.id.toString(), title: payload.title }); + }} + /> +{/if} diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..da5fbf1284890b3710c0c5b313e6446b81e333a8 --- /dev/null +++ b/src/lib/components/NavMenu.svelte @@ -0,0 +1,221 @@ + + + + + + +
+
+ {#each Object.entries(groupedConversations) as [group, convs]} + {#if convs.length} +

+ {titles[group]} +

+ {#each convs as conv} + + {/each} + {/if} + {/each} +
+ {#if hasMore} + + {/if} +
+
+ {#if user?.username || user?.email} +
+ {user?.username || user?.email} + + +
+ {/if} + + Models + {nModels} + + + + + Settings + + + +
diff --git a/src/lib/components/Pagination.svelte b/src/lib/components/Pagination.svelte new file mode 100644 index 0000000000000000000000000000000000000000..078410911ada587d9981c3afb417e904cd710612 --- /dev/null +++ b/src/lib/components/Pagination.svelte @@ -0,0 +1,97 @@ + + +{#if numTotalPages > 1} + +{/if} diff --git a/src/lib/components/PaginationArrow.svelte b/src/lib/components/PaginationArrow.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3310d2b657c6b94d7f3a45516faf228ff1bd2f24 --- /dev/null +++ b/src/lib/components/PaginationArrow.svelte @@ -0,0 +1,27 @@ + + + + {#if direction === "previous"} + + Previous + {:else} + Next + + {/if} + diff --git a/src/lib/components/Portal.svelte b/src/lib/components/Portal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..24971e607b47f58c51f360be309e37ba8fdae800 --- /dev/null +++ b/src/lib/components/Portal.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/RetryBtn.svelte b/src/lib/components/RetryBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7f94d8cddbbfcda0b2e72a2066bdf18bf418ffca --- /dev/null +++ b/src/lib/components/RetryBtn.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ScrollToBottomBtn.svelte b/src/lib/components/ScrollToBottomBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b897ea7e9dec8cfdbb4e17b5f96d383f0eca01b5 --- /dev/null +++ b/src/lib/components/ScrollToBottomBtn.svelte @@ -0,0 +1,47 @@ + + +{#if visible} + +{/if} diff --git a/src/lib/components/ScrollToPreviousBtn.svelte b/src/lib/components/ScrollToPreviousBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..68d65d8b14c1e028436ac5374e8e0be61d17334b --- /dev/null +++ b/src/lib/components/ScrollToPreviousBtn.svelte @@ -0,0 +1,77 @@ + + +{#if visible} + +{/if} diff --git a/src/lib/components/ShareConversationModal.svelte b/src/lib/components/ShareConversationModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..402d8874a9391905e3d19ef7cb633eaeb3a6aa4e --- /dev/null +++ b/src/lib/components/ShareConversationModal.svelte @@ -0,0 +1,182 @@ + + +{#if open} + +
+ + {#if createdUrl} +
+
+ Public link created +
+ +
+
+ A public link to your chat has been created. +
+ {:else} +
+
+ Share public link to chat +
+ +
+
+ Any messages you add after sharing stay private. +
+ {/if} + + {#if errorMsg} +
+ {errorMsg} +
+ {/if} + + +
+ + + {#if createdUrl} + { + justCopied = true; + oncopied?.(); + setTimeout(() => (justCopied = false), 1200); + }} + > + {#snippet children()} + + {#if justCopied} + + Copied + {:else} + + + Copy link + {/if} + + {/snippet} + + {:else} + + {/if} +
+
+
+{/if} diff --git a/src/lib/components/StopGeneratingBtn.svelte b/src/lib/components/StopGeneratingBtn.svelte new file mode 100644 index 0000000000000000000000000000000000000000..595b0da75b1f1e7076b56d62cd5800b7fd14b3c5 --- /dev/null +++ b/src/lib/components/StopGeneratingBtn.svelte @@ -0,0 +1,69 @@ + + + + + diff --git a/src/lib/components/SubscribeModal.svelte b/src/lib/components/SubscribeModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0883e2514b740298a9747ef43bc9579639b9f2a0 --- /dev/null +++ b/src/lib/components/SubscribeModal.svelte @@ -0,0 +1,80 @@ + + + +
+
+
+
+ + + + + + + + + + +
+

Upgrade Required

+
+
+ +
+

+ You've reached your message limit. Upgrade to Hugging Face PRO to continue using + HuggingChat. +

+

+ It's also possible to use your PRO credits in your favorite AI tools. +

+
+ +
+ + Upgrade to Pro + + +
+
+
diff --git a/src/lib/components/Switch.svelte b/src/lib/components/Switch.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4bc094edc51700d6c6c1e323ef6e030c8896cd6a --- /dev/null +++ b/src/lib/components/Switch.svelte @@ -0,0 +1,33 @@ + + + +
+
+
diff --git a/src/lib/components/SystemPromptModal.svelte b/src/lib/components/SystemPromptModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f58b026137df977dfb3538d6cb57ffc7d2b60de6 --- /dev/null +++ b/src/lib/components/SystemPromptModal.svelte @@ -0,0 +1,44 @@ + + + + +{#if isOpen} + (isOpen = false)} width="w-full !max-w-xl"> +
+
+

System Prompt

+ +
+ +
+
+{/if} diff --git a/src/lib/components/Toast.svelte b/src/lib/components/Toast.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fd78d7e4210a4aa12bbf12838e58ad144d6c9e97 --- /dev/null +++ b/src/lib/components/Toast.svelte @@ -0,0 +1,27 @@ + + + +
+
+ +

+ {message} +

+
+
+
diff --git a/src/lib/components/Tooltip.svelte b/src/lib/components/Tooltip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..af90602dd30091b1d7cc8e243d72dea8cccca30f --- /dev/null +++ b/src/lib/components/Tooltip.svelte @@ -0,0 +1,30 @@ + + +
+ + {label} +
diff --git a/src/lib/components/WelcomeModal.svelte b/src/lib/components/WelcomeModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b6f945550b8c8cbccf847180772a97a42282701a --- /dev/null +++ b/src/lib/components/WelcomeModal.svelte @@ -0,0 +1,56 @@ + + + +
+
+ + +
+ +
+

+ Welcome to {publicConfig.PUBLIC_APP_NAME}, the chat app powered by open source AI models. +

+

+ Omni automatically picks the best AI model to give + you optimal answers depending on your requests. +

+

+ You can also choose from any available open source models to chat with directly. +

+
+ + +
+
diff --git a/src/lib/components/chat/Alternatives.svelte b/src/lib/components/chat/Alternatives.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4973e258e00021c6554b9f60aab2c69519a0d2b0 --- /dev/null +++ b/src/lib/components/chat/Alternatives.svelte @@ -0,0 +1,77 @@ + + +
+ + + {currentIdx + 1} / {alternatives.length} + + + +
diff --git a/src/lib/components/chat/ChatInput.svelte b/src/lib/components/chat/ChatInput.svelte new file mode 100644 index 0000000000000000000000000000000000000000..66d30e35c391804e3689056bbf1fa35f7c00683a --- /dev/null +++ b/src/lib/components/chat/ChatInput.svelte @@ -0,0 +1,317 @@ + + +
+ + + {#if !showNoTools} +
+ {#if showFileUpload} +
+ { + if (requireAuthUser()) { + e.preventDefault(); + } + }} + accept={mimeTypes.join(",")} + /> + + + + + + + + {#if modelIsMultimodal} + openFilePickerImage()} + > + + Add image + + {/if} + + + +
+ + Add text file +
+
+ +
+
+ + openFilePickerText()} + > + + Upload from device + + (isUrlModalOpen = true)} + > + + Fetch from URL + + +
+
+
+
+
+ {/if} +
+ {/if} + {@render children?.()} + + +
+ + diff --git a/src/lib/components/chat/ChatIntroduction.svelte b/src/lib/components/chat/ChatIntroduction.svelte new file mode 100644 index 0000000000000000000000000000000000000000..16d9e773b6f21c3c6875c2cac225dcb7b7782292 --- /dev/null +++ b/src/lib/components/chat/ChatIntroduction.svelte @@ -0,0 +1,86 @@ + + +
+
+ + {publicConfig.PUBLIC_APP_NAME} +
+ +
diff --git a/src/lib/components/chat/ChatMessage.svelte b/src/lib/components/chat/ChatMessage.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e2f12fc0a1dd5b92b2b8a88f9ac18742d88aa9b0 --- /dev/null +++ b/src/lib/components/chat/ChatMessage.svelte @@ -0,0 +1,353 @@ + + +{#if message.from === "assistant"} + +{/if} +{#if message.from === "user"} + +{/if} + + diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte new file mode 100644 index 0000000000000000000000000000000000000000..702842c84cca6ccafc51802886e9d9ad2352a24d --- /dev/null +++ b/src/lib/components/chat/ChatWindow.svelte @@ -0,0 +1,675 @@ + + + { + e.preventDefault(); + }} + ondrop={(e) => { + e.preventDefault(); + onDrag = false; + }} +/> + +
+ {#if shareModalOpen} + shareModal.close()} /> + {/if} +
+
+ {#if preprompt && preprompt != currentModel.preprompt} + + {/if} + + {#if messages.length > 0} +
+ {#each messages as message, idx (message.id)} + a.includes(message.id)) ?? []} + isAuthor={!shared} + readOnly={isReadOnly} + isLast={idx === messages.length - 1} + bind:editMsdgId + onretry={(payload) => onretry?.(payload)} + onshowAlternateMsg={(payload) => onshowAlternateMsg?.(payload)} + /> + {/each} + {#if isReadOnly} + + {/if} +
+ {:else if pending} + + {:else} + { + onmessage?.(content); + }} + /> + {/if} +
+ + + + +
+ +
+ {#if !draft.length && !messages.length && !sources.length && !loading && currentModel.isRouter && routerExamples.length && !hideRouterExamples && !lastIsError} +
+ {#each routerExamples as ex} + + {/each} +
+ {/if} + {#if shouldShowRouterFollowUps && !lastIsError} +
+ + {#each routerFollowUps as followUp} + + {/each} +
+ {/if} + {#if sources?.length && !loading} +
+ {#each sources as source, index} + {#await source then src} + { + files = files.filter((_, i) => i !== index); + }} + /> + {/await} + {/each} +
+ {/if} + +
+
+ {#if !loading && lastIsError} + { + if (lastMessage && lastMessage.ancestors) { + onretry?.({ + id: lastMessage.id, + }); + } + }} + /> + {/if} +
+
{ + e.preventDefault(); + handleSubmit(); + }} + class={{ + "relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 dark:border-gray-700 dark:bg-gray-800": true, + "opacity-30": isReadOnly, + "max-sm:mb-4": focused && isVirtualKeyboard(), + }} + > + {#if onDrag && isFileUploadEnabled} + + {:else} +
+ {#if lastIsError} + + {:else} + + {/if} + + {#if loading} + onstop?.()} + showBorder={true} + classNames="absolute bottom-2 right-2 size-7 self-end rounded-full border bg-white text-black shadow transition-none dark:border-transparent dark:bg-gray-600 dark:text-white" + /> + {:else} + + {/if} +
+ {/if} + +
+ {#if models.find((m) => m.id === currentModel.id)} + {#if !currentModel.isRouter || !loading} + { + if (requireAuthUser()) { + e.preventDefault(); + } + }} + class="inline-flex items-center gap-1 hover:underline" + > + {#if currentModel.isRouter} + + {currentModel.displayName} + {:else} + Model: {currentModel.displayName} + {/if} + + + {:else if showRouterDetails && streamingRouterMetadata} +
+ + + + {streamingRouterMetadata.route} + + + with + + + {streamingRouterModelName} + +
+ {:else} +
+ Routing +
+ {/if} + {:else} + + {currentModel.id} + + {/if} + {#if !messages.length && !loading} + Generated content may be inaccurate or false. + {/if} +
+
+
+
+ + diff --git a/src/lib/components/chat/FileDropzone.svelte b/src/lib/components/chat/FileDropzone.svelte new file mode 100644 index 0000000000000000000000000000000000000000..380d4e6682c6859aa6f5a9de9e87fc87d00cba34 --- /dev/null +++ b/src/lib/components/chat/FileDropzone.svelte @@ -0,0 +1,92 @@ + + +
(onDragInner = true)} + ondragleave={() => (onDragInner = false)} + ondragover={(e) => { + e.preventDefault(); + }} + class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner + ? 'border-blue-200 !bg-blue-500/10 text-blue-600 *:pointer-events-none dark:border-blue-600 dark:bg-blue-500/20 dark:text-blue-500' + : 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}" +> + +

Drop File to add to chat

+
diff --git a/src/lib/components/chat/MarkdownBlock.svelte b/src/lib/components/chat/MarkdownBlock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..45f5957472a5c5a275f1823fb16be748b3226640 --- /dev/null +++ b/src/lib/components/chat/MarkdownBlock.svelte @@ -0,0 +1,23 @@ + + +{#each renderedTokens as token} + {#if token.type === "text"} + + {@html token.html} + {:else if token.type === "code"} + + {/if} +{/each} diff --git a/src/lib/components/chat/MarkdownRenderer.svelte b/src/lib/components/chat/MarkdownRenderer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2492cd783468e345810259be4de2b67dbde91b8d --- /dev/null +++ b/src/lib/components/chat/MarkdownRenderer.svelte @@ -0,0 +1,72 @@ + + +{#each blocks as block, index (loading && index === blocks.length - 1 ? `stream-${index}` : block.id)} + +{/each} diff --git a/src/lib/components/chat/MarkdownRenderer.svelte.test.ts b/src/lib/components/chat/MarkdownRenderer.svelte.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..375944bc4810433f931d5ca641967296948d4b6a --- /dev/null +++ b/src/lib/components/chat/MarkdownRenderer.svelte.test.ts @@ -0,0 +1,111 @@ +import MarkdownRenderer from "./MarkdownRenderer.svelte"; +import { render } from "vitest-browser-svelte"; +import { page } from "@vitest/browser/context"; + +import { describe, expect, it } from "vitest"; + +describe("MarkdownRenderer", () => { + it("renders", () => { + render(MarkdownRenderer, { content: "Hello, world!" }); + expect(page.getByText("Hello, world!")).toBeInTheDocument(); + }); + it("renders headings", () => { + render(MarkdownRenderer, { content: "# Hello, world!" }); + expect(page.getByRole("heading", { level: 1 })).toBeInTheDocument(); + }); + it("renders links", () => { + render(MarkdownRenderer, { content: "[Hello, world!](https://example.com)" }); + const link = page.getByRole("link", { name: "Hello, world!" }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "https://example.com"); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute("rel", "noreferrer"); + }); + it("renders inline codespans", () => { + render(MarkdownRenderer, { content: "`foobar`" }); + expect(page.getByRole("code")).toHaveTextContent("foobar"); + }); + it("renders block codes", () => { + render(MarkdownRenderer, { content: "```foobar```" }); + expect(page.getByRole("code")).toHaveTextContent("foobar"); + }); + it("renders sources correctly", () => { + const props = { + content: "Hello there [1]", + sources: [ + { + title: "foo", + link: "https://example.com", + }, + ], + }; + render(MarkdownRenderer, props); + + const link = page.getByRole("link"); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "https://example.com"); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute("rel", "noreferrer"); + }); + it("handles groups of sources", () => { + render(MarkdownRenderer, { + content: "Hello there [1], [2], [3]", + sources: [ + { + title: "foo", + link: "https://foo.com", + }, + { + title: "bar", + link: "https://bar.com", + }, + { + title: "baz", + link: "https://baz.com", + }, + ], + }); + expect(page.getByRole("link").all()).toHaveLength(3); + expect(page.getByRole("link").nth(0)).toHaveAttribute("href", "https://foo.com"); + expect(page.getByRole("link").nth(1)).toHaveAttribute("href", "https://bar.com"); + expect(page.getByRole("link").nth(2)).toHaveAttribute("href", "https://baz.com"); + }); + it("does not render sources in code blocks", () => { + render(MarkdownRenderer, { + content: "```\narray[1]\n```", + sources: [ + { + title: "foo", + link: "https://example.com", + }, + ], + }); + const linkSelector = page.getByRole("link"); + expect(linkSelector.elements).toHaveLength(0); + }); + it("doesnt render raw html directly", () => { + render(MarkdownRenderer, { content: "" }); + expect(page.getByRole("button").elements).toHaveLength(0); + expect(page.getByRole("paragraph")).toHaveTextContent(""); + }); + it("renders latex", () => { + const { baseElement } = render(MarkdownRenderer, { content: "$(oo)^2$" }); + expect(baseElement.querySelectorAll(".katex")).toHaveLength(1); + }); + it("does not render latex in code blocks", () => { + const { baseElement } = render(MarkdownRenderer, { content: "```\n$(oo)^2$\n```" }); + expect(baseElement.querySelectorAll(".katex")).toHaveLength(0); + }); + it("does not render latex in inline codes", () => { + const { baseElement } = render(MarkdownRenderer, { content: "`$oo` and `$bar`" }); + expect(baseElement.querySelectorAll(".katex")).toHaveLength(0); + }); + it("does not render latex across multiple lines", () => { + const { baseElement } = render(MarkdownRenderer, { content: "* $oo \n* $aa" }); + expect(baseElement.querySelectorAll(".katex")).toHaveLength(0); + }); + it("renders latex with some < and > symbols", () => { + const { baseElement } = render(MarkdownRenderer, { content: "$foo < bar > baz$" }); + expect(baseElement.querySelectorAll(".katex")).toHaveLength(1); + }); +}); diff --git a/src/lib/components/chat/MessageAvatar.svelte b/src/lib/components/chat/MessageAvatar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f2100fbd70109ce23e5da5d1d66451dbad404dff --- /dev/null +++ b/src/lib/components/chat/MessageAvatar.svelte @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/components/chat/ModelSwitch.svelte b/src/lib/components/chat/ModelSwitch.svelte new file mode 100644 index 0000000000000000000000000000000000000000..46863f470685ae5b68bd76bf948df0de69065b2c --- /dev/null +++ b/src/lib/components/chat/ModelSwitch.svelte @@ -0,0 +1,64 @@ + + +
+ + This model is no longer available. Switch to a new one to continue this conversation: + +
+ + +
+
diff --git a/src/lib/components/chat/OpenReasoningResults.svelte b/src/lib/components/chat/OpenReasoningResults.svelte new file mode 100644 index 0000000000000000000000000000000000000000..10080786d88b8d8b538b4d405f4d4a93dea5b7c2 --- /dev/null +++ b/src/lib/components/chat/OpenReasoningResults.svelte @@ -0,0 +1,86 @@ + + +
+ +
+
+ + + + {#if loading} + + + + {/if} + +
+
+
+
Reasoning
+
+ {summary.length > 33 + ? summary.substring(0, 33) + "..." + : summary.endsWith("...") + ? summary + : summary + "..."} +
+
+ +
+ +
+ {#key content} + + {/key} +
+
+ + diff --git a/src/lib/components/chat/UploadedFile.svelte b/src/lib/components/chat/UploadedFile.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8dcca28369d8eb5ea9b62ae5ee892aecb503af31 --- /dev/null +++ b/src/lib/components/chat/UploadedFile.svelte @@ -0,0 +1,253 @@ + + +{#if showModal && isClickable} + + (showModal = false)}> + {#if isImage(file.mime)} + {#if file.type === "hash"} + input from user + {:else} + + input from user + {/if} + {:else if isPlainText(file.mime)} +
+
+ +

{file.name}

+
+ {#if file.mime === "application/vnd.chatui.clipboard"} +

+ If you prefer to inject clipboard content directly in the chat, you can disable this + feature in the + settings page. +

+ {/if} + + {#if file.type === "hash"} + {#await fetch(urlNotTrailing + "/output/" + file.value).then((res) => res.text())} +
+ +
+ {:then result} +
{result}
+ {/await} + {:else} +
{atob(file.value)}
+ {/if} +
+ {/if} +
+{/if} + +
isClickable && (showModal = true)} + onkeydown={(e) => { + if (!isClickable) { + return; + } + if (e.key === "Enter" || e.key === " ") { + showModal = true; + } + }} + class:clickable={isClickable} + role="button" + tabindex="0" +> +
+ {#if isImage(file.mime)} +
+ {file.name} +
+ {:else if isAudio(file.mime)} + + {:else if isVideo(file.mime)} +
+ + +
+ {:else if isPlainText(file.mime)} +
+
+ +
+
+
+ {truncateMiddle(file.name, 28)} +
+ {#if file.mime === "application/vnd.chatui.clipboard"} +
Clipboard source
+ {:else} +
{file.mime}
+ {/if} +
+
+ {:else if file.mime === "application/octet-stream"} +
+
+ +
+
+
+ {truncateMiddle(file.name, 28)} +
+
File type could not be determined
+
+ + + +
+ {:else} +
+
+ +
+
+
+ {truncateMiddle(file.name, 28)} +
+
{file.mime}
+
+
+ {/if} + + {#if canClose} + + {/if} +
+
diff --git a/src/lib/components/chat/UrlFetchModal.svelte b/src/lib/components/chat/UrlFetchModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3dd4ca6c0599da3a8eaf72dc97c717aa0de0ab82 --- /dev/null +++ b/src/lib/components/chat/UrlFetchModal.svelte @@ -0,0 +1,190 @@ + + +{#if open} + + {#snippet children()} +
{ + e.preventDefault(); + handleSubmit(); + }} + > +
+

Add from URL

+ +
+ +
+ + { + if (e.key === "Enter") { + e.preventDefault(); + handleSubmit(); + } + }} + /> +
+ + {#if errorMsg} +

{errorMsg}

+ {/if} +

Only HTTPS. Max 10MB.

+ +
+ + +
+
+ {/snippet} +
+{/if} + + diff --git a/src/lib/components/icons/IconBurger.svelte b/src/lib/components/icons/IconBurger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..64a13801479d562cbac3bb7ed56b28cf2b85b303 --- /dev/null +++ b/src/lib/components/icons/IconBurger.svelte @@ -0,0 +1,20 @@ + + + + diff --git a/src/lib/components/icons/IconChevron.svelte b/src/lib/components/icons/IconChevron.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a0d17dc029483411400ebf3eeb8833fb963fd90f --- /dev/null +++ b/src/lib/components/icons/IconChevron.svelte @@ -0,0 +1,24 @@ + + + + + diff --git a/src/lib/components/icons/IconDazzled.svelte b/src/lib/components/icons/IconDazzled.svelte new file mode 100644 index 0000000000000000000000000000000000000000..764ca7c78e04da39f69858854115d8e612220e5d --- /dev/null +++ b/src/lib/components/icons/IconDazzled.svelte @@ -0,0 +1,40 @@ + + + + + + + + + + + + diff --git a/src/lib/components/icons/IconLoading.svelte b/src/lib/components/icons/IconLoading.svelte new file mode 100644 index 0000000000000000000000000000000000000000..78b754b29303d3a56422146e232299bf90ae7956 --- /dev/null +++ b/src/lib/components/icons/IconLoading.svelte @@ -0,0 +1,22 @@ + + +
+
+
+
+
diff --git a/src/lib/components/icons/IconMoon.svelte b/src/lib/components/icons/IconMoon.svelte new file mode 100644 index 0000000000000000000000000000000000000000..efab26aff5ba6c7558427f2f8c455dea6348eca4 --- /dev/null +++ b/src/lib/components/icons/IconMoon.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/icons/IconNew.svelte b/src/lib/components/icons/IconNew.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3ac50480d922f60ae621992c7cb089c061a84205 --- /dev/null +++ b/src/lib/components/icons/IconNew.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/IconOmni.svelte b/src/lib/components/icons/IconOmni.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c027809a872dc6e2d84c78a077016a429dd3b54e --- /dev/null +++ b/src/lib/components/icons/IconOmni.svelte @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/components/icons/IconPaperclip.svelte b/src/lib/components/icons/IconPaperclip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a5d236b7cec2939eae799a44e6016ff527e4c155 --- /dev/null +++ b/src/lib/components/icons/IconPaperclip.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/icons/IconShare.svelte b/src/lib/components/icons/IconShare.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f1cbae541dde035850f967cf980d012cef1514b4 --- /dev/null +++ b/src/lib/components/icons/IconShare.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/icons/IconSun.svelte b/src/lib/components/icons/IconSun.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f06c96b5ec8a2e30602eee7b21de1b1f8510fbee --- /dev/null +++ b/src/lib/components/icons/IconSun.svelte @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/components/icons/Logo.svelte b/src/lib/components/icons/Logo.svelte new file mode 100644 index 0000000000000000000000000000000000000000..011ce8b00506f992eecea7aeea81d0cf9ec0f131 --- /dev/null +++ b/src/lib/components/icons/Logo.svelte @@ -0,0 +1,17 @@ + + +{publicConfig.PUBLIC_APP_NAME} logo diff --git a/src/lib/components/icons/LogoHuggingFaceBorderless.svelte b/src/lib/components/icons/LogoHuggingFaceBorderless.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0f1cc6062f9dbbaae49bd12ed476af3fc336665d --- /dev/null +++ b/src/lib/components/icons/LogoHuggingFaceBorderless.svelte @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/components/players/AudioPlayer.svelte b/src/lib/components/players/AudioPlayer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e95baf2411f823cb57ec90c65f35d95029a63c46 --- /dev/null +++ b/src/lib/components/players/AudioPlayer.svelte @@ -0,0 +1,82 @@ + + +
+ + + +
+
{name}
+ {#if duration !== Infinity} +
+ {format(time)} +
{ + paused = true; + }} + onpointerup={seek} + > +
+
+ {duration ? format(duration) : "--:--"} +
+ {/if} +
+
diff --git a/src/lib/constants/mime.ts b/src/lib/constants/mime.ts new file mode 100644 index 0000000000000000000000000000000000000000..77608d20d8172cf9b66442029304e216b238c954 --- /dev/null +++ b/src/lib/constants/mime.ts @@ -0,0 +1,11 @@ +// Centralized MIME allowlists used across client and server +// Keep these lists minimal and consistent with server processing. + +export const TEXT_MIME_ALLOWLIST = [ + "text/*", + "application/json", + "application/xml", + "application/csv", +] as const; + +export const IMAGE_MIME_ALLOWLIST_DEFAULT = ["image/jpeg", "image/png"] as const; diff --git a/src/lib/constants/pagination.ts b/src/lib/constants/pagination.ts new file mode 100644 index 0000000000000000000000000000000000000000..a054569f16aea5df77d71973f4881d8fdd8722ad --- /dev/null +++ b/src/lib/constants/pagination.ts @@ -0,0 +1 @@ +export const CONV_NUM_PER_PAGE = 30; diff --git a/src/lib/constants/publicSepToken.ts b/src/lib/constants/publicSepToken.ts new file mode 100644 index 0000000000000000000000000000000000000000..15d962d69ba33e1abeb8a35885aa7647d24cf7af --- /dev/null +++ b/src/lib/constants/publicSepToken.ts @@ -0,0 +1 @@ +export const PUBLIC_SEP_TOKEN = ""; diff --git a/src/lib/constants/routerExamples.ts b/src/lib/constants/routerExamples.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0495914a5620b1d487cac24660406181f00fc9b --- /dev/null +++ b/src/lib/constants/routerExamples.ts @@ -0,0 +1,209 @@ +export type RouterFollowUp = { + title: string; + prompt: string; +}; + +export type RouterExampleAttachment = { + src: string; +}; + +export type RouterExample = { + title: string; + prompt: string; + followUps?: RouterFollowUp[]; + attachments?: RouterExampleAttachment[]; +}; + +export const routerExamples: RouterExample[] = [ + { + title: "HTML game", + prompt: "Code a minimal Flappy Bird game using HTML and Canvas", + followUps: [ + { + title: "README.md file", + prompt: "Create a comprehensive README.md for the Flappy Bird game project.", + }, + { + title: "CRT Screen", + prompt: "Add a CRT screen effect to the game", + }, + { + title: "Add power-ups", + prompt: + "Add collectible coins between pipes that award bonus points and a shield power-up that allows one collision.", + }, + { + title: "Explain collision detection", + prompt: + "Explain the collision detection algorithm for the bird and pipes in simple terms with examples.", + }, + ], + }, + { + title: "Weird painting", + prompt: "is this a real painting?", + attachments: [ + { + src: "huggingchat/castle-example.jpg", + }, + ], + }, + { + title: "Landing page", + prompt: + "Build a responsive SaaS landing page for my AI coding assitant using Tailwind CSS. With a hero, features, testimonials, and pricing sections.", + followUps: [ + { + title: "Dark mode", + prompt: "Add dark mode and make it the default", + }, + { + title: "Write blog post", + prompt: "Write a blog post introducing my service.", + }, + { + title: "Translate to Italian", + prompt: "Translate only the text content displayed to users into Italian.", + }, + { + title: "Architecture review", + prompt: + "Review the architecture and suggest improvements for scalability, SEO optimization, and performance.", + }, + ], + }, + { + title: "Eminem song", + prompt: + "Write an Eminem-style rap battling AI taking over hip-hop, with two energetic verses and a catchy hook.", + followUps: [ + { + title: "Psychological analysis", + prompt: "Provide a psychological analysis of Eminem's emotions in this song.", + }, + { + title: "Wired Article", + prompt: "Write an article in the style of Wired explaining this Eminem release.", + }, + { + title: "Roleplay", + prompt: "Roleplay as Eminem so I can discuss the song with him.", + }, + { + title: "Translate to Spanish", + prompt: "Translate the rap lyrics to Spanish while maintaining the rhyme scheme and flow.", + }, + ], + }, + { + title: "Act as Yoda", + prompt: "Act as Yoda", + followUps: [ + { + title: "Give advice", + prompt: + "Continue acting as Yoda and offer three pieces of life advice for staying focused under pressure.", + }, + { + title: "Explain the Force", + prompt: + "In Yoda's voice, explain the concept of the Force to a young padawan using modern language.", + }, + { + title: "Plain English", + prompt: + "Rewrite the previous response from Yoda into plain English while keeping the same meaning.", + }, + { + title: "Compare philosophies", + prompt: + "Compare Yoda's Jedi philosophy to Stoic philosophy from ancient Greece and explain the similarities and differences.", + }, + ], + }, + { + title: "Generate prompts", + prompt: `Generate 5 creative prompts Text-to-image prompts like: "Cyberpunk cityscape at night, neon lights, flying cars, rain-slicked streets, blade runner aesthetic, highly detailed`, + followUps: [ + { + title: "Turn into JSON", + prompt: `Generate a detailed JSON object for each prompt. Include fields for subjects (list of objects), scene (setting, environment, background details), actions (what's happening), style (artistic style or medium)`, + }, + { + title: "Sci-fi portraits", + prompt: + "Produce five futuristic character portrait prompts with unique professions and settings.", + }, + { + title: "Explain image generation", + prompt: + "Explain how text-to-image diffusion models work, covering the denoising process and how text prompts guide generation.", + }, + ], + }, + { + title: "Explain LLMs", + prompt: + "Explain how large language models based on transformers work, covering attention, embeddings, and training objectives.", + followUps: [ + { + title: "Generate a Quiz", + prompt: "Craft a 5-question multiple-choice quiz to validate what I learned.", + }, + { + title: "Compare to RNNs", + prompt: + "Compare transformer-based large language models to recurrent neural networks, focusing on training efficiency and capabilities.", + }, + { + title: "Student summary", + prompt: + "Summarize the explanation of large language models for a high school student using relatable analogies.", + }, + { + title: "Write a blog post", + prompt: + "Write a blog post about how transformers revolutionized NLP, targeting software engineers who are new to AI.", + }, + ], + }, + { + title: "Translate in Italian", + prompt: `Translate in Italian: Some are born great, some achieve greatness, and some have greatness thrust upon 'em`, + followUps: [ + { + title: "Back to English", + prompt: + "Translate the Italian version back into English while keeping Shakespeare's tone intact.", + }, + { + title: "Explain choices", + prompt: "Explain your translation choices for each key phrase from the Italian version.", + }, + { + title: "Modernize", + prompt: + "Modernize the Italian translation into contemporary informal Italian suitable for social media.", + }, + { + title: "Teach me Italian", + prompt: + "Help me practice Italian by conversing about this Shakespeare quote, correcting my grammar when needed.", + }, + ], + }, + { + title: "Pelican on a bicycle", + prompt: "Draw an SVG of a pelican riding a bicycle", + followUps: [ + { + title: "Add a top hat", + prompt: "Add a fancy top hat to the pelican and make it look distinguished", + }, + { + title: "Make it animated", + prompt: "Add CSS animations to make the bicycle wheels spin and the pelican's wings flap", + }, + ], + }, +]; diff --git a/src/lib/createShareLink.ts b/src/lib/createShareLink.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1f9446ae851a45d64cf255b6e478ce915d4a0cb --- /dev/null +++ b/src/lib/createShareLink.ts @@ -0,0 +1,27 @@ +import { base } from "$app/paths"; +import { page } from "$app/state"; + +// Returns a public share URL for a conversation id. +// If `id` is already a 7-char share id, no network call is made. +export async function createShareLink(id: string): Promise { + const prefix = + page.data.publicConfig.PUBLIC_SHARE_PREFIX || + `${page.data.publicConfig.PUBLIC_ORIGIN || page.url.origin}${base}`; + + if (id.length === 7) { + return `${prefix}/r/${id}`; + } + + const res = await fetch(`${base}/conversation/${id}/share`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || "Failed to create share link"); + } + + const { shareId } = await res.json(); + return `${prefix}/r/${shareId}`; +} diff --git a/src/lib/jobs/refresh-conversation-stats.ts b/src/lib/jobs/refresh-conversation-stats.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3e8a0421f1cede0c0d83ea975c6b4bde9585589 --- /dev/null +++ b/src/lib/jobs/refresh-conversation-stats.ts @@ -0,0 +1,276 @@ +import type { ConversationStats } from "$lib/types/ConversationStats"; +import { CONVERSATION_STATS_COLLECTION, collections } from "$lib/server/database"; +import { logger } from "$lib/server/logger"; +import type { ObjectId } from "mongodb"; +import { acquireLock, refreshLock } from "$lib/migrations/lock"; +import { Semaphores } from "$lib/types/Semaphore"; + +async function getLastComputationTime(): Promise { + const lastStats = await collections.conversationStats.findOne({}, { sort: { "date.at": -1 } }); + return lastStats?.date?.at || new Date(0); +} + +async function shouldComputeStats(): Promise { + const lastComputationTime = await getLastComputationTime(); + const oneDayAgo = new Date(Date.now() - 24 * 3_600_000); + return lastComputationTime < oneDayAgo; +} + +export async function computeAllStats() { + for (const span of ["day", "week", "month"] as const) { + computeStats({ dateField: "updatedAt", type: "conversation", span }).catch((e) => + logger.error(e) + ); + computeStats({ dateField: "createdAt", type: "conversation", span }).catch((e) => + logger.error(e) + ); + computeStats({ dateField: "createdAt", type: "message", span }).catch((e) => logger.error(e)); + } +} + +async function computeStats(params: { + dateField: ConversationStats["date"]["field"]; + span: ConversationStats["date"]["span"]; + type: ConversationStats["type"]; +}) { + const indexes = await collections.semaphores.listIndexes().toArray(); + if (indexes.length <= 2) { + logger.info("Indexes not created, skipping stats computation"); + return; + } + + const lastComputed = await collections.conversationStats.findOne( + { "date.field": params.dateField, "date.span": params.span, type: params.type }, + { sort: { "date.at": -1 } } + ); + + // If the last computed week is at the beginning of the last computed month, we need to include some days from the previous month + // In those cases we need to compute the stats from before the last month as everything is one aggregation + const minDate = lastComputed ? lastComputed.date.at : new Date(0); + + logger.debug( + { minDate, dateField: params.dateField, span: params.span, type: params.type }, + "Computing conversation stats" + ); + + const dateField = params.type === "message" ? "messages." + params.dateField : params.dateField; + + const pipeline = [ + { + $match: { + [dateField]: { $gte: minDate }, + }, + }, + { + $project: { + [dateField]: 1, + sessionId: 1, + userId: 1, + }, + }, + ...(params.type === "message" + ? [ + { + $unwind: "$messages", + }, + { + $match: { + [dateField]: { $gte: minDate }, + }, + }, + ] + : []), + { + $sort: { + [dateField]: 1, + }, + }, + { + $facet: { + userId: [ + { + $match: { + userId: { $exists: true }, + }, + }, + { + $group: { + _id: { + at: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + userId: "$userId", + }, + }, + }, + { + $group: { + _id: "$_id.at", + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "userId", + count: 1, + }, + }, + ], + sessionId: [ + { + $match: { + sessionId: { $exists: true }, + }, + }, + { + $group: { + _id: { + at: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + sessionId: "$sessionId", + }, + }, + }, + { + $group: { + _id: "$_id.at", + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "sessionId", + count: 1, + }, + }, + ], + userOrSessionId: [ + { + $group: { + _id: { + at: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + userOrSessionId: { $ifNull: ["$userId", "$sessionId"] }, + }, + }, + }, + { + $group: { + _id: "$_id.at", + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "userOrSessionId", + count: 1, + }, + }, + ], + _id: [ + { + $group: { + _id: { $dateTrunc: { date: `$${dateField}`, unit: params.span } }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + date: { + at: "$_id", + field: params.dateField, + span: params.span, + }, + distinct: "_id", + count: 1, + }, + }, + ], + }, + }, + { + $project: { + stats: { + $concatArrays: ["$userId", "$sessionId", "$userOrSessionId", "$_id"], + }, + }, + }, + { + $unwind: "$stats", + }, + { + $replaceRoot: { + newRoot: "$stats", + }, + }, + { + $set: { + type: params.type, + }, + }, + { + $merge: { + into: CONVERSATION_STATS_COLLECTION, + on: ["date.at", "type", "date.span", "date.field", "distinct"], + whenMatched: "replace", + whenNotMatched: "insert", + }, + }, + ]; + + await collections.conversations.aggregate(pipeline, { allowDiskUse: true }).next(); + + logger.debug( + { minDate, dateField: params.dateField, span: params.span, type: params.type }, + "Computed conversation stats" + ); +} + +let hasLock = false; +let lockId: ObjectId | null = null; + +async function maintainLock() { + if (hasLock && lockId) { + hasLock = await refreshLock(Semaphores.CONVERSATION_STATS, lockId); + + if (!hasLock) { + lockId = null; + } + } else if (!hasLock) { + lockId = (await acquireLock(Semaphores.CONVERSATION_STATS)) || null; + hasLock = !!lockId; + } + + setTimeout(maintainLock, 10_000); +} + +export function refreshConversationStats() { + const ONE_HOUR_MS = 3_600_000; + + maintainLock().then(async () => { + if (await shouldComputeStats()) { + computeAllStats(); + } + + setInterval(async () => { + if (await shouldComputeStats()) { + computeAllStats(); + } + }, 24 * ONE_HOUR_MS); + }); +} diff --git a/src/lib/migrations/lock.ts b/src/lib/migrations/lock.ts new file mode 100644 index 0000000000000000000000000000000000000000..f542b0d576a551b73fb3a34994e7fa5661ab2cc8 --- /dev/null +++ b/src/lib/migrations/lock.ts @@ -0,0 +1,56 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import type { Semaphores } from "$lib/types/Semaphore"; + +/** + * Returns the lock id if the lock was acquired, false otherwise + */ +export async function acquireLock(key: Semaphores | string): Promise { + try { + const id = new ObjectId(); + + const insert = await collections.semaphores.insertOne({ + _id: id, + key, + createdAt: new Date(), + updatedAt: new Date(), + deleteAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes + }); + + return insert.acknowledged ? id : false; // true if the document was inserted + } catch (e) { + // unique index violation, so there must already be a lock + return false; + } +} + +export async function releaseLock(key: Semaphores | string, lockId: ObjectId) { + await collections.semaphores.deleteOne({ + _id: lockId, + key, + }); +} + +export async function isDBLocked(key: Semaphores | string): Promise { + const res = await collections.semaphores.countDocuments({ + key, + }); + return res > 0; +} + +export async function refreshLock(key: Semaphores | string, lockId: ObjectId): Promise { + const result = await collections.semaphores.updateOne( + { + _id: lockId, + key, + }, + { + $set: { + updatedAt: new Date(), + deleteAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes + }, + } + ); + + return result.matchedCount > 0; +} diff --git a/src/lib/migrations/migrations.spec.ts b/src/lib/migrations/migrations.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e0879355e7de382a079402edc1cb9d1f8f7fa65 --- /dev/null +++ b/src/lib/migrations/migrations.spec.ts @@ -0,0 +1,74 @@ +import { afterEach, assert, beforeAll, describe, expect, it } from "vitest"; +import { migrations } from "./routines"; +import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock"; +import { Semaphores } from "$lib/types/Semaphore"; +import { collections, ready } from "$lib/server/database"; + +describe( + "migrations", + { + retry: 3, + }, + () => { + beforeAll(async () => { + await ready; + try { + await collections.semaphores.createIndex({ key: 1 }, { unique: true }); + } catch (e) { + // Index might already exist, ignore error + } + }); + + it("should not have duplicates guid", async () => { + const guids = migrations.map((m) => m._id.toString()); + const uniqueGuids = [...new Set(guids)]; + expect(uniqueGuids.length).toBe(guids.length); + }); + + it("should acquire only one lock on DB", async () => { + const results = await Promise.all( + new Array(1000).fill(0).map(() => acquireLock(Semaphores.TEST_MIGRATION)) + ); + const locks = results.filter((r) => r); + + const semaphores = await collections.semaphores.find({}).toArray(); + + expect(locks.length).toBe(1); + expect(semaphores).toBeDefined(); + expect(semaphores.length).toBe(1); + expect(semaphores?.[0].key).toBe(Semaphores.TEST_MIGRATION); + }); + + it("should read the lock correctly", async () => { + const lockId = await acquireLock(Semaphores.TEST_MIGRATION); + assert(lockId); + expect(await isDBLocked(Semaphores.TEST_MIGRATION)).toBe(true); + expect(!!(await acquireLock(Semaphores.TEST_MIGRATION))).toBe(false); + await releaseLock(Semaphores.TEST_MIGRATION, lockId); + expect(await isDBLocked(Semaphores.TEST_MIGRATION)).toBe(false); + }); + + it("should refresh the lock", async () => { + const lockId = await acquireLock(Semaphores.TEST_MIGRATION); + + assert(lockId); + + // get the updatedAt time + + const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt; + + await refreshLock(Semaphores.TEST_MIGRATION, lockId); + + const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt; + + expect(updatedAtInitially).toBeDefined(); + expect(updatedAtAfterRefresh).toBeDefined(); + expect(updatedAtInitially).not.toBe(updatedAtAfterRefresh); + }); + + afterEach(async () => { + await collections.semaphores.deleteMany({}); + await collections.migrationResults.deleteMany({}); + }); + } +); diff --git a/src/lib/migrations/migrations.ts b/src/lib/migrations/migrations.ts new file mode 100644 index 0000000000000000000000000000000000000000..386982fd3825b1727788fe8df2bdec913ebb4572 --- /dev/null +++ b/src/lib/migrations/migrations.ts @@ -0,0 +1,118 @@ +import { Database } from "$lib/server/database"; +import { migrations } from "./routines"; +import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock"; +import { Semaphores } from "$lib/types/Semaphore"; +import { logger } from "$lib/server/logger"; +import { config } from "$lib/server/config"; + +export async function checkAndRunMigrations() { + // make sure all GUIDs are unique + if (new Set(migrations.map((m) => m._id.toString())).size !== migrations.length) { + throw new Error("Duplicate migration GUIDs found."); + } + + // check if all migrations have already been run + const migrationResults = await (await Database.getInstance()) + .getCollections() + .migrationResults.find() + .toArray(); + + logger.debug("[MIGRATIONS] Begin check..."); + + // connect to the database + const connectedClient = await (await Database.getInstance()).getClient().connect(); + + const lockId = await acquireLock(Semaphores.MIGRATION); + + if (!lockId) { + // another instance already has the lock, so we exit early + logger.debug( + "[MIGRATIONS] Another instance already has the lock. Waiting for DB to be unlocked." + ); + + // Todo: is this necessary? Can we just return? + // block until the lock is released + while (await isDBLocked(Semaphores.MIGRATION)) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + return; + } + + // once here, we have the lock + // make sure to refresh it regularly while it's running + const refreshInterval = setInterval(async () => { + await refreshLock(Semaphores.MIGRATION, lockId); + }, 1000 * 10); + + // iterate over all migrations + for (const migration of migrations) { + // check if the migration has already been applied + const shouldRun = + migration.runEveryTime || + !migrationResults.find((m) => m._id.toString() === migration._id.toString()); + + // check if the migration has already been applied + if (!shouldRun) { + logger.debug(`[MIGRATIONS] "${migration.name}" already applied. Skipping...`); + } else { + // check the modifiers to see if some cases match + if ( + (migration.runForHuggingChat === "only" && !config.isHuggingChat) || + (migration.runForHuggingChat === "never" && config.isHuggingChat) + ) { + logger.debug( + `[MIGRATIONS] "${migration.name}" should not be applied for this run. Skipping...` + ); + continue; + } + + // otherwise all is good and we can run the migration + logger.debug( + `[MIGRATIONS] "${migration.name}" ${ + migration.runEveryTime ? "should run every time" : "not applied yet" + }. Applying...` + ); + + await (await Database.getInstance()).getCollections().migrationResults.updateOne( + { _id: migration._id }, + { + $set: { + name: migration.name, + status: "ongoing", + }, + }, + { upsert: true } + ); + + const session = connectedClient.startSession(); + let result = false; + + try { + await session.withTransaction(async () => { + result = await migration.up(await Database.getInstance()); + }); + } catch (e) { + logger.debug(`[MIGRATIONS] "${migration.name}" failed!`); + logger.error(e); + } finally { + await session.endSession(); + } + + await (await Database.getInstance()).getCollections().migrationResults.updateOne( + { _id: migration._id }, + { + $set: { + name: migration.name, + status: result ? "success" : "failure", + }, + }, + { upsert: true } + ); + } + } + + logger.debug("[MIGRATIONS] All migrations applied. Releasing lock"); + + clearInterval(refreshInterval); + await releaseLock(Semaphores.MIGRATION, lockId); +} diff --git a/src/lib/migrations/routines/01-update-search-assistants.ts b/src/lib/migrations/routines/01-update-search-assistants.ts new file mode 100644 index 0000000000000000000000000000000000000000..52c8b2f6c99a5e9d349690271c4e28761e351b53 --- /dev/null +++ b/src/lib/migrations/routines/01-update-search-assistants.ts @@ -0,0 +1,50 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId, type AnyBulkWriteOperation } from "mongodb"; +import type { Assistant } from "$lib/types/Assistant"; +import { generateSearchTokens } from "$lib/utils/searchTokens"; + +const migration: Migration = { + _id: new ObjectId("5f9f3e3e3e3e3e3e3e3e3e3e"), + name: "Update search assistants", + up: async () => { + const { assistants } = collections; + let ops: AnyBulkWriteOperation[] = []; + + for await (const assistant of assistants + .find() + .project>({ _id: 1, name: 1 })) { + ops.push({ + updateOne: { + filter: { + _id: assistant._id, + }, + update: { + $set: { + searchTokens: generateSearchTokens(assistant.name), + }, + }, + }, + }); + + if (ops.length >= 1000) { + process.stdout.write("."); + await assistants.bulkWrite(ops, { ordered: false }); + ops = []; + } + } + + if (ops.length) { + await assistants.bulkWrite(ops, { ordered: false }); + } + + return true; + }, + down: async () => { + const { assistants } = collections; + await assistants.updateMany({}, { $unset: { searchTokens: "" } }); + return true; + }, +}; + +export default migration; diff --git a/src/lib/migrations/routines/02-update-assistants-models.ts b/src/lib/migrations/routines/02-update-assistants-models.ts new file mode 100644 index 0000000000000000000000000000000000000000..855abb665e47740748e861c2103b7648c08dd0d6 --- /dev/null +++ b/src/lib/migrations/routines/02-update-assistants-models.ts @@ -0,0 +1,48 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; + +const updateAssistantsModels: Migration = { + _id: new ObjectId("5f9f3f3f3f3f3f3f3f3f3f3f"), + name: "Update deprecated models in assistants with the default model", + up: async () => { + const models = (await import("$lib/server/models")).models; + //@ts-expect-error the property doesn't exist anymore, keeping the script for reference + const oldModels = (await import("$lib/server/models")).oldModels; + const { assistants } = collections; + + const modelIds = models.map((el) => el.id); + const defaultModelId = models[0].id; + + // Find all assistants whose modelId is not in modelIds, and update it + const bulkOps = await assistants + .find({ modelId: { $nin: modelIds } }) + .map((assistant) => { + // has an old model + let newModelId = defaultModelId; + + const oldModel = oldModels.find((m: (typeof models)[number]) => m.id === assistant.modelId); + if (oldModel && oldModel.transferTo && !!models.find((m) => m.id === oldModel.transferTo)) { + newModelId = oldModel.transferTo; + } + + return { + updateOne: { + filter: { _id: assistant._id }, + update: { $set: { modelId: newModelId } }, + }, + }; + }) + .toArray(); + + if (bulkOps.length > 0) { + await assistants.bulkWrite(bulkOps); + } + + return true; + }, + runEveryTime: true, + runForHuggingChat: "only", +}; + +export default updateAssistantsModels; diff --git a/src/lib/migrations/routines/04-update-message-updates.ts b/src/lib/migrations/routines/04-update-message-updates.ts new file mode 100644 index 0000000000000000000000000000000000000000..4617d2c86096d2ba312f8f638657d9730f7f3b2e --- /dev/null +++ b/src/lib/migrations/routines/04-update-message-updates.ts @@ -0,0 +1,151 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId, type WithId } from "mongodb"; +import type { Conversation } from "$lib/types/Conversation"; +import { + MessageUpdateStatus, + MessageUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; +import type { Message } from "$lib/types/Message"; +// isMessageWebSearchSourcesUpdate removed from utils; use inline predicate + +// ----------- +// Copy of the previous message update types +export type FinalAnswer = { + type: "finalAnswer"; + text: string; +}; + +export type TextStreamUpdate = { + type: "stream"; + token: string; +}; + +type WebSearchUpdate = { + type: "webSearch"; + messageType: "update" | "error" | "sources"; + message: string; + args?: string[]; + sources?: { title?: string; link: string }[]; +}; + +type StatusUpdate = { + type: "status"; + status: "started" | "pending" | "finished" | "error" | "title"; + message?: string; +}; + +type ErrorUpdate = { + type: "error"; + message: string; + name: string; +}; + +type FileUpdate = { + type: "file"; + sha: string; +}; + +type OldMessageUpdate = + | FinalAnswer + | TextStreamUpdate + | WebSearchUpdate + | StatusUpdate + | ErrorUpdate + | FileUpdate; + +/** Converts the old message update to the new schema */ +function convertMessageUpdate(message: Message, update: OldMessageUpdate): MessageUpdate | null { + try { + // Text and files + if (update.type === "finalAnswer") { + return { + type: MessageUpdateType.FinalAnswer, + text: update.text, + interrupted: message.interrupted ?? false, + }; + } else if (update.type === "stream") { + return { + type: MessageUpdateType.Stream, + token: update.token, + }; + } else if (update.type === "file") { + return { + type: MessageUpdateType.File, + name: "Unknown", + sha: update.sha, + // assume jpeg but could be any image. should be harmless + mime: "image/jpeg", + }; + } + + // Status + else if (update.type === "status") { + if (update.status === "title") { + return { + type: MessageUpdateType.Title, + title: update.message ?? "New Chat", + }; + } + if (update.status === "pending") return null; + + const status = + update.status === "started" + ? MessageUpdateStatus.Started + : update.status === "finished" + ? MessageUpdateStatus.Finished + : MessageUpdateStatus.Error; + return { + type: MessageUpdateType.Status, + status, + message: update.message, + }; + } else if (update.type === "error") { + // Treat it as an error status update + return { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: update.message, + }; + } + + // Web Search + else if (update.type === "webSearch") { + return null; // Web search updates are no longer supported + } + console.warn("Unknown message update during migration:", update); + return null; + } catch (error) { + console.error("Error converting message update during migration. Skipping it... Error:", error); + return null; + } +} + +const updateMessageUpdates: Migration = { + _id: new ObjectId("5f9f7f7f7f7f7f7f7f7f7f7f"), + name: "Convert message updates to the new schema", + up: async () => { + const allConversations = collections.conversations.find({}); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + // Convert all of the existing updates to the new schema + const updates = message.updates + ?.map((update) => convertMessageUpdate(message, update as OldMessageUpdate)) + .filter((update): update is MessageUpdate => Boolean(update)); + + return { ...message, updates }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default updateMessageUpdates; diff --git a/src/lib/migrations/routines/05-update-message-files.ts b/src/lib/migrations/routines/05-update-message-files.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a91cb86aa399ce9b55732fbf598b1ec5cc077a4 --- /dev/null +++ b/src/lib/migrations/routines/05-update-message-files.ts @@ -0,0 +1,56 @@ +import { ObjectId, type WithId } from "mongodb"; +import { collections } from "$lib/server/database"; + +import type { Migration } from "."; +import type { Conversation } from "$lib/types/Conversation"; +import type { MessageFile } from "$lib/types/Message"; + +const updateMessageFiles: Migration = { + _id: new ObjectId("5f9f5f5f5f5f5f5f5f5f5f5f"), + name: "Convert message files to the new schema", + up: async () => { + const allConversations = collections.conversations.find({}, { projection: { messages: 1 } }); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + const files = (message.files as string[] | undefined)?.map((file) => { + // File is already in the new format + if (typeof file !== "string") return file; + + // File was a hash pointing to a file in the bucket + if (file.length === 64) { + return { + type: "hash", + name: "unknown.jpg", + value: file, + mime: "image/jpeg", + }; + } + // File was a base64 string + else { + return { + type: "base64", + name: "unknown.jpg", + value: file, + mime: "image/jpeg", + }; + } + }); + + return { + ...message, + files, + }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default updateMessageFiles; diff --git a/src/lib/migrations/routines/06-trim-message-updates.ts b/src/lib/migrations/routines/06-trim-message-updates.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b0a8564c7450e6fee7b4aaf59d4ee3fcd43fd56 --- /dev/null +++ b/src/lib/migrations/routines/06-trim-message-updates.ts @@ -0,0 +1,56 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId, type WithId } from "mongodb"; +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { MessageUpdate } from "$lib/types/MessageUpdate"; +import { logger } from "$lib/server/logger"; + +// ----------- + +/** Converts the old message update to the new schema */ +function convertMessageUpdate(message: Message, update: unknown): MessageUpdate | null { + try { + // Trim legacy web search updates entirely + if ( + typeof update === "object" && + update !== null && + (update as { type: string }).type === "webSearch" + ) { + return null; + } + + return update as MessageUpdate; + } catch (error) { + logger.error(error, "Error converting message update during migration. Skipping it.."); + return null; + } +} + +const trimMessageUpdates: Migration = { + _id: new ObjectId("000000000000000000000006"), + name: "Trim message updates to reduce stored size", + up: async () => { + const allConversations = collections.conversations.find({}); + + let conversation: WithId> | null = null; + while ((conversation = await allConversations.tryNext())) { + const messages = conversation.messages.map((message) => { + // Convert all of the existing updates to the new schema + const updates = message.updates + ?.map((update) => convertMessageUpdate(message, update)) + .filter((update): update is MessageUpdate => Boolean(update)); + + return { ...message, updates }; + }); + + // Set the new messages array + await collections.conversations.updateOne({ _id: conversation._id }, { $set: { messages } }); + } + + return true; + }, + runEveryTime: false, +}; + +export default trimMessageUpdates; diff --git a/src/lib/migrations/routines/08-update-featured-to-review.ts b/src/lib/migrations/routines/08-update-featured-to-review.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ac5d8e2d8c73744afaa2a292a349a6a12796703 --- /dev/null +++ b/src/lib/migrations/routines/08-update-featured-to-review.ts @@ -0,0 +1,32 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { ReviewStatus } from "$lib/types/Review"; + +const updateFeaturedToReview: Migration = { + _id: new ObjectId("000000000000000000000008"), + name: "Update featured to review", + up: async () => { + const { assistants, tools } = collections; + + // Update assistants + await assistants.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } }); + await assistants.updateMany( + { featured: { $ne: true } }, + { $set: { review: ReviewStatus.PRIVATE } } + ); + + await assistants.updateMany({}, { $unset: { featured: "" } }); + + // Update tools + await tools.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } }); + await tools.updateMany({ featured: { $ne: true } }, { $set: { review: ReviewStatus.PRIVATE } }); + + await tools.updateMany({}, { $unset: { featured: "" } }); + + return true; + }, + runEveryTime: false, +}; + +export default updateFeaturedToReview; diff --git a/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts b/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f807e61917ece332f305c852f236ae372385e20e --- /dev/null +++ b/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts @@ -0,0 +1,214 @@ +import type { Session } from "$lib/types/Session"; +import type { User } from "$lib/types/User"; +import type { Conversation } from "$lib/types/Conversation"; +import { ObjectId } from "mongodb"; +import { deleteConversations } from "./09-delete-empty-conversations"; +import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest"; +import { collections } from "$lib/server/database"; + +type Message = Conversation["messages"][number]; + +const userData = { + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username: "new-username", + name: "name", + avatarUrl: "https://example.com/avatar.png", + hfUserId: "9999999999", +} satisfies User; +Object.freeze(userData); + +const sessionForUser = { + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + userId: userData._id, + sessionId: "session-id-9999999999", + expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), +} satisfies Session; +Object.freeze(sessionForUser); + +const userMessage = { + from: "user", + id: "user-message-id", + content: "Hello, how are you?", +} satisfies Message; + +const assistantMessage = { + from: "assistant", + id: "assistant-message-id", + content: "I'm fine, thank you!", +} satisfies Message; + +const systemMessage = { + from: "system", + id: "system-message-id", + content: "This is a system message", +} satisfies Message; + +const conversationBase = { + _id: new ObjectId(), + createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + updatedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + model: "model-id", + + title: "title", + messages: [], +} satisfies Conversation; + +describe.sequential("Deleting discarded conversations", async () => { + test("a conversation with no messages should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("a conversation with no messages that is less than 1 hour old should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + createdAt: new Date(Date.now() - 30 * 60 * 1000), + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with only system messages should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [systemMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("a conversation with a user message should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [userMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with an assistant message should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [assistantMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with a mix of messages should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + sessionId: sessionForUser.sessionId, + messages: [systemMessage, userMessage, assistantMessage, userMessage, assistantMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with a userId and no sessionId should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + userId: userData._id, + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with no userId or sessionId should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("a conversation with a sessionId that exists should not get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + sessionId: sessionForUser.sessionId, + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with a userId and a sessionId that doesn't exist should NOT get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + userId: userData._id, + messages: [userMessage, assistantMessage], + sessionId: new ObjectId().toString(), + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(0); + }); + test("a conversation with only a sessionId that doesn't exist, should get deleted", async () => { + await collections.conversations.insertOne({ + ...conversationBase, + messages: [userMessage, assistantMessage], + sessionId: new ObjectId().toString(), + }); + + const result = await deleteConversations(collections); + + expect(result).toBe(1); + }); + test("many conversations should get deleted", async () => { + const conversations = Array.from({ length: 10010 }, () => ({ + ...conversationBase, + _id: new ObjectId(), + })); + + await collections.conversations.insertMany(conversations); + + const result = await deleteConversations(collections); + + expect(result).toBe(10010); + }); +}); + +beforeAll(async () => { + await collections.users.insertOne(userData); + await collections.sessions.insertOne(sessionForUser); +}); + +afterAll(async () => { + await collections.users.deleteOne({ + _id: userData._id, + }); + await collections.sessions.deleteOne({ + _id: sessionForUser._id, + }); + await collections.conversations.deleteMany({}); +}); + +afterEach(async () => { + await collections.conversations.deleteMany({ + _id: { $in: [conversationBase._id] }, + }); +}); diff --git a/src/lib/migrations/routines/09-delete-empty-conversations.ts b/src/lib/migrations/routines/09-delete-empty-conversations.ts new file mode 100644 index 0000000000000000000000000000000000000000..30ada9110e50bd4d5e3de0df6233b43e05492821 --- /dev/null +++ b/src/lib/migrations/routines/09-delete-empty-conversations.ts @@ -0,0 +1,88 @@ +import type { Migration } from "."; +import { collections } from "$lib/server/database"; +import { Collection, FindCursor, ObjectId } from "mongodb"; +import { logger } from "$lib/server/logger"; +import type { Conversation } from "$lib/types/Conversation"; + +const BATCH_SIZE = 1000; +const DELETE_THRESHOLD_MS = 60 * 60 * 1000; + +async function deleteBatch(conversations: Collection, ids: ObjectId[]) { + if (ids.length === 0) return 0; + const deleteResult = await conversations.deleteMany({ _id: { $in: ids } }); + return deleteResult.deletedCount; +} + +async function processCursor( + cursor: FindCursor, + processBatchFn: (batch: T[]) => Promise +) { + let batch = []; + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (doc) { + batch.push(doc); + } + if (batch.length >= BATCH_SIZE) { + await processBatchFn(batch); + batch = []; + } + } + if (batch.length > 0) { + await processBatchFn(batch); + } +} + +export async function deleteConversations( + collections: typeof import("$lib/server/database").collections +) { + let deleteCount = 0; + const { conversations, sessions } = collections; + + // First criteria: Delete conversations with no user/assistant messages older than 1 hour + const emptyConvCursor = conversations + .find({ + "messages.from": { $not: { $in: ["user", "assistant"] } }, + createdAt: { $lt: new Date(Date.now() - DELETE_THRESHOLD_MS) }, + }) + .batchSize(BATCH_SIZE); + + await processCursor(emptyConvCursor, async (batch) => { + const ids = batch.map((doc) => doc._id); + deleteCount += await deleteBatch(conversations, ids); + }); + + // Second criteria: Process conversations without users in batches and check sessions + const noUserCursor = conversations.find({ userId: { $exists: false } }).batchSize(BATCH_SIZE); + + await processCursor(noUserCursor, async (batch) => { + const sessionIds = [ + ...new Set(batch.map((conv) => conv.sessionId).filter((id): id is string => !!id)), + ]; + + const existingSessions = await sessions.find({ sessionId: { $in: sessionIds } }).toArray(); + const validSessionIds = new Set(existingSessions.map((s) => s.sessionId)); + + const invalidConvs = batch.filter( + (conv) => !conv.sessionId || !validSessionIds.has(conv.sessionId) + ); + const idsToDelete = invalidConvs.map((conv) => conv._id); + deleteCount += await deleteBatch(conversations, idsToDelete); + }); + + logger.info(`[MIGRATIONS] Deleted ${deleteCount} conversations in total.`); + return deleteCount; +} + +const deleteEmptyConversations: Migration = { + _id: new ObjectId("000000000000000000000009"), + name: "Delete conversations with no user or assistant messages or valid sessions", + up: async () => { + await deleteConversations(collections); + return true; + }, + runEveryTime: false, + runForHuggingChat: "only", +}; + +export default deleteEmptyConversations; diff --git a/src/lib/migrations/routines/10-update-reports-assistantid.ts b/src/lib/migrations/routines/10-update-reports-assistantid.ts new file mode 100644 index 0000000000000000000000000000000000000000..95ef89c2e80a4786e677ca9e95b5b68410bd8fff --- /dev/null +++ b/src/lib/migrations/routines/10-update-reports-assistantid.ts @@ -0,0 +1,29 @@ +import { collections } from "$lib/server/database"; +import type { Migration } from "."; +import { ObjectId } from "mongodb"; + +const migration: Migration = { + _id: new ObjectId("000000000000000000000010"), + name: "Update reports with assistantId to use contentId", + up: async () => { + await collections.reports.updateMany( + { + assistantId: { $exists: true, $ne: null }, + }, + [ + { + $set: { + object: "assistant", + contentId: "$assistantId", + }, + }, + { + $unset: "assistantId", + }, + ] + ); + return true; + }, +}; + +export default migration; diff --git a/src/lib/migrations/routines/index.ts b/src/lib/migrations/routines/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..119bacf4fe309e8f7aaa1cae33b9de397ba3901c --- /dev/null +++ b/src/lib/migrations/routines/index.ts @@ -0,0 +1,15 @@ +import type { ObjectId } from "mongodb"; + +import type { Database } from "$lib/server/database"; + +export interface Migration { + _id: ObjectId; + name: string; + up: (client: Database) => Promise; + down?: (client: Database) => Promise; + runForFreshInstall?: "only" | "never"; // leave unspecified to run for both + runForHuggingChat?: "only" | "never"; // leave unspecified to run for both + runEveryTime?: boolean; +} + +export const migrations: Migration[] = []; diff --git a/src/lib/server/abortRegistry.ts b/src/lib/server/abortRegistry.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc6de8a4413da0de1f18415da5c327d3fd8f3f33 --- /dev/null +++ b/src/lib/server/abortRegistry.ts @@ -0,0 +1,57 @@ +import { logger } from "$lib/server/logger"; + +/** + * Tracks active upstream generation requests so they can be cancelled on demand. + * Multiple controllers can be registered per conversation (for threaded/background runs). + */ +export class AbortRegistry { + private static instance: AbortRegistry; + + private controllers = new Map>(); + + public static getInstance(): AbortRegistry { + if (!AbortRegistry.instance) { + AbortRegistry.instance = new AbortRegistry(); + } + return AbortRegistry.instance; + } + + public register(conversationId: string, controller: AbortController) { + const key = conversationId.toString(); + let set = this.controllers.get(key); + if (!set) { + set = new Set(); + this.controllers.set(key, set); + } + set.add(controller); + controller.signal.addEventListener( + "abort", + () => { + this.unregister(key, controller); + }, + { once: true } + ); + } + + public abort(conversationId: string) { + const set = this.controllers.get(conversationId); + if (!set?.size) return; + + logger.debug({ conversationId }, "Aborting active generation via AbortRegistry"); + for (const controller of set) { + if (!controller.signal.aborted) { + controller.abort(); + } + } + this.controllers.delete(conversationId); + } + + public unregister(conversationId: string, controller: AbortController) { + const set = this.controllers.get(conversationId); + if (!set) return; + set.delete(controller); + if (set.size === 0) { + this.controllers.delete(conversationId); + } + } +} diff --git a/src/lib/server/abortedGenerations.ts b/src/lib/server/abortedGenerations.ts new file mode 100644 index 0000000000000000000000000000000000000000..57b5f738b913d24957c8bfe9f1effbac0d205701 --- /dev/null +++ b/src/lib/server/abortedGenerations.ts @@ -0,0 +1,42 @@ +// Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 + +import { logger } from "$lib/server/logger"; +import { collections } from "$lib/server/database"; +import { onExit } from "./exitHandler"; + +export class AbortedGenerations { + private static instance: AbortedGenerations; + + private abortedGenerations: Record = {}; + + private constructor() { + const interval = setInterval(() => this.updateList(), 1000); + onExit(() => clearInterval(interval)); + + this.updateList(); + } + + public static getInstance(): AbortedGenerations { + if (!AbortedGenerations.instance) { + AbortedGenerations.instance = new AbortedGenerations(); + } + + return AbortedGenerations.instance; + } + + public getAbortTime(conversationId: string): Date | undefined { + return this.abortedGenerations[conversationId]; + } + + private async updateList() { + try { + const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray(); + + this.abortedGenerations = Object.fromEntries( + aborts.map((abort) => [abort.conversationId.toString(), abort.createdAt]) + ); + } catch (err) { + logger.error(err); + } + } +} diff --git a/src/lib/server/adminToken.ts b/src/lib/server/adminToken.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9dbfd0eacd9fd65f4c41eb014e239ba0718d154 --- /dev/null +++ b/src/lib/server/adminToken.ts @@ -0,0 +1,62 @@ +import { config } from "$lib/server/config"; +import type { Session } from "$lib/types/Session"; +import { logger } from "./logger"; +import { v4 } from "uuid"; + +class AdminTokenManager { + private token = config.ADMIN_TOKEN || v4(); + // contains all session ids that are currently admin sessions + private adminSessions: Array = []; + + public get enabled() { + // if open id is configured, disable the feature + return config.ADMIN_CLI_LOGIN === "true"; + } + public isAdmin(sessionId: Session["sessionId"]) { + if (!this.enabled) return false; + return this.adminSessions.includes(sessionId); + } + + public checkToken(token: string, sessionId: Session["sessionId"]) { + if (!this.enabled) return false; + if (token === this.token) { + logger.info(`[ADMIN] Token validated`); + this.adminSessions.push(sessionId); + this.token = config.ADMIN_TOKEN || v4(); + return true; + } + + return false; + } + + public removeSession(sessionId: Session["sessionId"]) { + this.adminSessions = this.adminSessions.filter((id) => id !== sessionId); + } + + public displayToken() { + // if admin token is set, don't display it + if (!this.enabled || config.ADMIN_TOKEN) return; + + let port = process.env.PORT + ? parseInt(process.env.PORT) + : process.argv.includes("--port") + ? parseInt(process.argv[process.argv.indexOf("--port") + 1]) + : undefined; + + if (!port) { + const mode = process.argv.find((arg) => arg === "preview" || arg === "dev"); + if (mode === "preview") { + port = 4173; + } else if (mode === "dev") { + port = 5173; + } else { + port = 3000; + } + } + + const url = (config.PUBLIC_ORIGIN || `http://localhost:${port}`) + "?token="; + logger.info(`[ADMIN] You can login with ${url + this.token}`); + } +} + +export const adminTokenManager = new AdminTokenManager(); diff --git a/src/lib/server/api/authPlugin.ts b/src/lib/server/api/authPlugin.ts new file mode 100644 index 0000000000000000000000000000000000000000..ccf135bd120e27d88bb1c920efbd28814f7aa947 --- /dev/null +++ b/src/lib/server/api/authPlugin.ts @@ -0,0 +1,29 @@ +import Elysia from "elysia"; +import { authenticateRequest } from "../auth"; +import { config } from "../config"; + +export const authPlugin = new Elysia({ name: "auth" }).derive( + { as: "scoped" }, + async ({ + headers, + cookie, + request, + }): Promise<{ + locals: App.Locals; + }> => { + request.url; + const auth = await authenticateRequest( + { type: "elysia", value: headers }, + { type: "elysia", value: cookie }, + new URL(request.url, config.PUBLIC_ORIGIN), + true + ); + return { + locals: { + user: auth?.user, + sessionId: auth?.sessionId, + isAdmin: auth?.isAdmin, + }, + }; + } +); diff --git a/src/lib/server/api/index.ts b/src/lib/server/api/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e63df3e86946afed9c5dfc2e2906f0acc48eb2b --- /dev/null +++ b/src/lib/server/api/index.ts @@ -0,0 +1,48 @@ +import { authPlugin } from "$api/authPlugin"; +import { conversationGroup } from "$api/routes/groups/conversations"; +import { userGroup } from "$api/routes/groups/user"; +import { misc } from "$api/routes/groups/misc"; +import { modelGroup } from "$api/routes/groups/models"; +import { debugGroup } from "$api/routes/groups/debug"; + +import { Elysia } from "elysia"; +import { base } from "$app/paths"; +import { swagger } from "@elysiajs/swagger"; +import { config } from "$lib/server/config"; + +import superjson from "superjson"; + +const prefix = `${base}/api/v2` as unknown as ""; + +export const app = new Elysia({ prefix }) + .mapResponse(({ response, request }) => { + // Skip the /export endpoint + if (request.url.endsWith("/export")) { + return response as unknown as Response; + } + return new Response(superjson.stringify(response), { + headers: { + "Content-Type": "application/json", + }, + }); + }) + .use( + swagger({ + documentation: { + info: { + title: "chat-ui API", + version: config.PUBLIC_VERSION, + }, + }, + provider: "swagger-ui", + path: `swagger`, + }) + ) + .use(authPlugin) + .use(conversationGroup) + .use(userGroup) + .use(modelGroup) + .use(misc) + .use(debugGroup); + +export type App = typeof app; diff --git a/src/lib/server/api/routes/groups/conversations.ts b/src/lib/server/api/routes/groups/conversations.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d7ef5396eacf28e6434eb7524111048b6239666 --- /dev/null +++ b/src/lib/server/api/routes/groups/conversations.ts @@ -0,0 +1,275 @@ +import { Elysia, error, t } from "elysia"; +import { authPlugin } from "$api/authPlugin"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { authCondition } from "$lib/server/auth"; +import { validModelIdSchema } from "$lib/server/models"; +import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation"; +import type { Conversation } from "$lib/types/Conversation"; + +import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination"; + +export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => { + return ( + app + .guard({ + as: "scoped", + beforeHandle: async ({ locals }) => { + if (!locals.user?._id && !locals.sessionId) { + return error(401, "Must have a valid session or user"); + } + }, + }) + .get( + "", + async ({ locals, query }) => { + const convs = await collections.conversations + .find(authCondition(locals)) + .project>({ + title: 1, + updatedAt: 1, + model: 1, + }) + .sort({ updatedAt: -1 }) + .skip((query.p ?? 0) * CONV_NUM_PER_PAGE) + .limit(CONV_NUM_PER_PAGE) + .toArray(); + + const nConversations = await collections.conversations.countDocuments( + authCondition(locals) + ); + + const res = convs.map((conv) => ({ + _id: conv._id, + id: conv._id, // legacy param iOS + title: conv.title, + updatedAt: conv.updatedAt, + model: conv.model, + modelId: conv.model, // legacy param iOS + })); + + return { conversations: res, nConversations }; + }, + { + query: t.Object({ + p: t.Optional(t.Number()), + }), + } + ) + .delete("", async ({ locals }) => { + const res = await collections.conversations.deleteMany({ + ...authCondition(locals), + }); + return res.deletedCount; + }) + // search endpoint removed + .group( + "/:id", + { + params: t.Object({ + id: t.String(), + }), + }, + (app) => { + return app + .derive(async ({ locals, params, query }) => { + let conversation; + let shared = false; + + // if the conversation is shared + if (params.id.length === 7) { + // shared link of length 7 + conversation = await collections.sharedConversations.findOne({ + _id: params.id, + }); + shared = true; + if (!conversation) { + throw new Error("Conversation not found"); + } + } else { + // todo: add validation on params.id + try { + new ObjectId(params.id); + } catch { + throw new Error("Invalid conversation ID format"); + } + conversation = await collections.conversations.findOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + if (!conversation) { + const conversationExists = + (await collections.conversations.countDocuments({ + _id: new ObjectId(params.id), + })) !== 0; + + if (conversationExists) { + throw new Error( + "You don't have access to this conversation. If someone gave you this link, ask them to use the 'share' feature instead." + ); + } + + throw new Error("Conversation not found."); + } + if (query.fromShare && conversation.meta?.fromShareId === query.fromShare) { + shared = true; + } + } + + const convertedConv = { + ...conversation, + ...convertLegacyConversation(conversation), + shared, + }; + + return { conversation: convertedConv }; + }) + .get( + "", + async ({ conversation }) => { + return { + messages: conversation.messages, + title: conversation.title, + model: conversation.model, + preprompt: conversation.preprompt, + rootMessageId: conversation.rootMessageId, + id: conversation._id.toString(), + updatedAt: conversation.updatedAt, + modelId: conversation.model, + shared: conversation.shared, + }; + }, + { + query: t.Optional( + t.Object({ + fromShare: t.Optional(t.String()), + }) + ), + } + ) + .post("", () => { + // todo: post new message + throw new Error("Not implemented"); + }) + .delete("", async ({ locals, params }) => { + const res = await collections.conversations.deleteOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + if (res.deletedCount === 0) { + throw new Error("Conversation not found"); + } + + return { success: true }; + }) + .get("/output/:sha256", () => { + // todo: get output + throw new Error("Not implemented"); + }) + .post("/share", () => { + // todo: share conversation + throw new Error("Not implemented"); + }) + .post("/stop-generating", () => { + // todo: stop generating + throw new Error("Not implemented"); + }) + .patch( + "", + async ({ locals, params, body }) => { + if (body.model) { + if (!validModelIdSchema.safeParse(body.model).success) { + throw new Error("Invalid model ID"); + } + } + + // Only include defined values in the update (sanitize title) + const updateValues = { + ...(body.title !== undefined && { + title: body.title.replace(/<\/?think>/gi, "").trim(), + }), + ...(body.model !== undefined && { model: body.model }), + }; + + const res = await collections.conversations.updateOne( + { + _id: new ObjectId(params.id), + ...authCondition(locals), + }, + { + $set: updateValues, + } + ); + + // Use matchedCount if available (newer drivers), fallback to modifiedCount for compatibility + if ( + typeof res.matchedCount === "number" + ? res.matchedCount === 0 + : res.modifiedCount === 0 + ) { + throw new Error("Conversation not found"); + } + + return { success: true }; + }, + { + body: t.Object({ + title: t.Optional( + t.String({ + minLength: 1, + maxLength: 100, + }) + ), + model: t.Optional(t.String()), + }), + } + ) + .delete( + "/message/:messageId", + async ({ locals, params, conversation }) => { + if (!conversation.messages.map((m) => m.id).includes(params.messageId)) { + throw new Error("Message not found"); + } + + const filteredMessages = conversation.messages + .filter( + (message) => + // not the message AND the message is not in ancestors + !(message.id === params.messageId) && + message.ancestors && + !message.ancestors.includes(params.messageId) + ) + .map((message) => { + // remove the message from children if it's there + if (message.children && message.children.includes(params.messageId)) { + message.children = message.children.filter( + (child) => child !== params.messageId + ); + } + return message; + }); + + const res = await collections.conversations.updateOne( + { _id: new ObjectId(conversation._id), ...authCondition(locals) }, + { $set: { messages: filteredMessages } } + ); + + if (res.modifiedCount === 0) { + throw new Error("Deleting message failed"); + } + + return { success: true }; + }, + { + params: t.Object({ + id: t.String(), + messageId: t.String(), + }), + } + ); + } + ) + ); +}); diff --git a/src/lib/server/api/routes/groups/debug.ts b/src/lib/server/api/routes/groups/debug.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d2a4891e06543d7ffddb58e34c37a9391c95c55 --- /dev/null +++ b/src/lib/server/api/routes/groups/debug.ts @@ -0,0 +1,43 @@ +import { Elysia } from "elysia"; +import { config } from "$lib/server/config"; + +export const debugGroup = new Elysia().group("/debug", (app) => + app + .get("/config", async () => { + const { models } = await import("$lib/server/models"); + return { + OPENAI_BASE_URL: config.OPENAI_BASE_URL, + OPENAI_API_KEY_SET: Boolean(config.OPENAI_API_KEY || config.HF_TOKEN), + LEGACY_HF_TOKEN_SET: Boolean(config.HF_TOKEN && !config.OPENAI_API_KEY), + MODELS_COUNT: models.length, + NODE_VERSION: process.versions.node, + }; + }) + .get("/refresh", async () => { + const base = (config.OPENAI_BASE_URL || "https://router.huggingface.co/v1").replace( + /\/$/, + "" + ); + const res = await fetch(`${base}/models`); + const body = await res.text(); + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch (_err) { + parsed = undefined; + } + return { + status: res.status, + ok: res.ok, + base, + length: (() => { + if (parsed && typeof parsed === "object" && "data" in parsed) { + const data = (parsed as { data?: unknown }).data; + return Array.isArray(data) ? data.length : null; + } + return null; + })(), + sample: body.slice(0, 2000), + }; + }) +); diff --git a/src/lib/server/api/routes/groups/misc.ts b/src/lib/server/api/routes/groups/misc.ts new file mode 100644 index 0000000000000000000000000000000000000000..948f10bd68eaf4a77f83785be964c689d96103e9 --- /dev/null +++ b/src/lib/server/api/routes/groups/misc.ts @@ -0,0 +1,211 @@ +import { Elysia } from "elysia"; +import { authPlugin } from "../../authPlugin"; +import { loginEnabled } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { authCondition } from "$lib/server/auth"; +import { config } from "$lib/server/config"; +import yazl from "yazl"; +import { downloadFile } from "$lib/server/files/downloadFile"; +import mimeTypes from "mime-types"; +import { logger } from "$lib/server/logger"; + +export interface FeatureFlags { + enableAssistants: boolean; + loginEnabled: boolean; + isAdmin: boolean; +} + +export const misc = new Elysia() + .use(authPlugin) + .get("/public-config", async () => config.getPublicConfig()) + .get("/feature-flags", async ({ locals }) => { + return { + enableAssistants: config.ENABLE_ASSISTANTS === "true", + loginEnabled, // login feature is on when OID is configured + isAdmin: locals.isAdmin, + } satisfies FeatureFlags; + }) + .get("/export", async ({ locals }) => { + if (!locals.user) { + throw new Error("Not logged in"); + } + + if (!locals.isAdmin) { + throw new Error("Not admin"); + } + + if (config.ENABLE_DATA_EXPORT !== "true") { + throw new Error("Data export is not enabled"); + } + + const nExports = await collections.messageEvents.countDocuments({ + userId: locals.user._id, + type: "export", + expiresAt: { $gt: new Date() }, + }); + + if (nExports >= 1) { + throw new Error( + "You have already exported your data recently. Please wait 1 hour before exporting again." + ); + } + + const stats: { + nConversations: number; + nMessages: number; + nFiles: number; + nAssistants: number; + nAvatars: number; + } = { + nConversations: 0, + nMessages: 0, + nFiles: 0, + nAssistants: 0, + nAvatars: 0, + }; + + const zipfile = new yazl.ZipFile(); + + const promises = [ + collections.conversations + .find({ ...authCondition(locals) }) + .toArray() + .then(async (conversations) => { + const formattedConversations = await Promise.all( + conversations.map(async (conversation) => { + stats.nConversations++; + const hashes: string[] = []; + conversation.messages.forEach(async (message) => { + stats.nMessages++; + if (message.files) { + message.files.forEach((file) => { + hashes.push(file.value); + }); + } + }); + const files = await Promise.all( + hashes.map(async (hash) => { + try { + const fileData = await downloadFile(hash, conversation._id); + return fileData; + } catch { + return null; + } + }) + ); + + const filenames: string[] = []; + files.forEach((file) => { + if (!file) return; + + const extension = mimeTypes.extension(file.mime) || null; + const convId = conversation._id.toString(); + const fileId = file.name.split("-")[1].slice(0, 8); + const fileName = `file-${convId}-${fileId}` + (extension ? `.${extension}` : ""); + filenames.push(fileName); + zipfile.addBuffer(Buffer.from(file.value, "base64"), fileName); + stats.nFiles++; + }); + + return { + ...conversation, + messages: conversation.messages.map((message) => { + return { + ...message, + files: filenames, + updates: undefined, + }; + }), + }; + }) + ); + + zipfile.addBuffer( + Buffer.from(JSON.stringify(formattedConversations, null, 2)), + "conversations.json" + ); + }), + collections.assistants + .find({ createdById: locals.user._id }) + .toArray() + .then(async (assistants) => { + const formattedAssistants = await Promise.all( + assistants.map(async (assistant) => { + if (assistant.avatar) { + const fileId = collections.bucket.find({ filename: assistant._id.toString() }); + + const content = await fileId.next().then(async (file) => { + if (!file?._id) return; + + const fileStream = collections.bucket.openDownloadStream(file?._id); + + const fileBuffer = await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + fileStream.on("data", (chunk) => chunks.push(chunk)); + fileStream.on("error", reject); + fileStream.on("end", () => resolve(Buffer.concat(chunks))); + }); + + return fileBuffer; + }); + + if (!content) return; + + zipfile.addBuffer(content, `avatar-${assistant._id.toString()}.jpg`); + stats.nAvatars++; + } + + stats.nAssistants++; + + return { + _id: assistant._id.toString(), + name: assistant.name, + createdById: assistant.createdById.toString(), + createdByName: assistant.createdByName, + avatar: `avatar-${assistant._id.toString()}.jpg`, + modelId: assistant.modelId, + preprompt: assistant.preprompt, + description: assistant.description, + dynamicPrompt: assistant.dynamicPrompt, + exampleInputs: assistant.exampleInputs, + generateSettings: assistant.generateSettings, + createdAt: assistant.createdAt.toISOString(), + updatedAt: assistant.updatedAt.toISOString(), + }; + }) + ); + + zipfile.addBuffer( + Buffer.from(JSON.stringify(formattedAssistants, null, 2)), + "assistants.json" + ); + }), + ]; + + Promise.all(promises).then(async () => { + logger.info( + { + userId: locals.user?._id, + ...stats, + }, + "Exported user data" + ); + zipfile.end(); + if (locals.user?._id) { + await collections.messageEvents.insertOne({ + userId: locals.user?._id, + type: "export", + createdAt: new Date(), + expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour + }); + } + }); + + // @ts-expect-error - zipfile.outputStream is not typed correctly + return new Response(zipfile.outputStream, { + headers: { + "Content-Type": "application/zip", + "Content-Disposition": 'attachment; filename="export.zip"', + }, + }); + }); diff --git a/src/lib/server/api/routes/groups/models.ts b/src/lib/server/api/routes/groups/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..87e2573807c506712394cd612702c5a83ffc916c --- /dev/null +++ b/src/lib/server/api/routes/groups/models.ts @@ -0,0 +1,155 @@ +import { Elysia, status } from "elysia"; +import { refreshModels, lastModelRefreshSummary } from "$lib/server/models"; +import type { BackendModel } from "$lib/server/models"; +import { authPlugin } from "../../authPlugin"; +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; + +export type GETModelsResponse = Array<{ + id: string; + name: string; + websiteUrl?: string; + modelUrl?: string; + datasetName?: string; + datasetUrl?: string; + displayName: string; + description?: string; + logoUrl?: string; + providers?: Array<{ provider: string } & Record>; + promptExamples?: { title: string; prompt: string }[]; + parameters: BackendModel["parameters"]; + preprompt?: string; + multimodal: boolean; + multimodalAcceptedMimetypes?: string[]; + unlisted: boolean; + hasInferenceAPI: boolean; + // Mark router entry for UI decoration — always present + isRouter: boolean; +}>; + +export type GETOldModelsResponse = Array<{ + id: string; + name: string; + displayName: string; + transferTo?: string; +}>; + +export const modelGroup = new Elysia().group("/models", (app) => + app + .get("/", async () => { + try { + const { models } = await import("$lib/server/models"); + return models + .filter((m) => m.unlisted == false) + .map((model) => ({ + id: model.id, + name: model.name, + websiteUrl: model.websiteUrl, + modelUrl: model.modelUrl, + datasetName: model.datasetName, + datasetUrl: model.datasetUrl, + displayName: model.displayName, + description: model.description, + logoUrl: model.logoUrl, + providers: model.providers as unknown as Array< + { provider: string } & Record + >, + promptExamples: model.promptExamples, + parameters: model.parameters, + preprompt: model.preprompt, + multimodal: model.multimodal, + multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes, + unlisted: model.unlisted, + hasInferenceAPI: model.hasInferenceAPI, + isRouter: model.isRouter, + })) satisfies GETModelsResponse; + } catch (e) { + // Return empty list instead of crashing the whole page + return [] as GETModelsResponse; + } + }) + .get("/old", async () => { + return [] as GETOldModelsResponse; + }) + .group("/refresh", (app) => + app.use(authPlugin).post("", async ({ locals }) => { + if (!locals.user && !locals.sessionId) { + throw status(401, "Unauthorized"); + } + if (!locals.isAdmin) { + throw status(403, "Admin privileges required"); + } + + const previous = lastModelRefreshSummary; + + try { + const summary = await refreshModels(); + + return { + refreshedAt: summary.refreshedAt.toISOString(), + durationMs: summary.durationMs, + added: summary.added, + removed: summary.removed, + changed: summary.changed, + total: summary.total, + hadChanges: + summary.added.length > 0 || summary.removed.length > 0 || summary.changed.length > 0, + previous: + previous.refreshedAt.getTime() > 0 + ? { + refreshedAt: previous.refreshedAt.toISOString(), + total: previous.total, + } + : null, + }; + } catch (err) { + throw status(502, "Model refresh failed"); + } + }) + ) + .group("/:namespace/:model?", (app) => + app + .derive(async ({ params, error }) => { + let modelId: string = params.namespace; + if (params.model) { + modelId += "/" + params.model; + } + try { + const { models } = await import("$lib/server/models"); + const model = models.find((m) => m.id === modelId); + if (!model || model.unlisted) { + return error(404, "Model not found"); + } + return { model }; + } catch (e) { + return error(500, "Models not available"); + } + }) + .get("/", ({ model }) => { + return model; + }) + .use(authPlugin) + .post("/subscribe", async ({ locals, model, error }) => { + if (!locals.sessionId) { + return error(401, "Unauthorized"); + } + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + activeModel: model.id, + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + + return new Response(); + }) + ) +); diff --git a/src/lib/server/api/routes/groups/user.ts b/src/lib/server/api/routes/groups/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..21eadca78a36d927d36870da7a0d63f2aed8c450 --- /dev/null +++ b/src/lib/server/api/routes/groups/user.ts @@ -0,0 +1,127 @@ +import { Elysia } from "elysia"; +import { authPlugin } from "$api/authPlugin"; +import { defaultModel } from "$lib/server/models"; +import { collections } from "$lib/server/database"; +import { authCondition } from "$lib/server/auth"; +import { models, validateModel } from "$lib/server/models"; +import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings"; +import { z } from "zod"; + +export const userGroup = new Elysia() + .use(authPlugin) + .get("/login", () => { + // todo: login + throw new Error("Not implemented"); + }) + .get("/login/callback", () => { + // todo: login callback + throw new Error("Not implemented"); + }) + .post("/logout", () => { + // todo: logout + throw new Error("Not implemented"); + }) + .group("/user", (app) => { + return app + .get("/", ({ locals }) => { + return locals.user + ? { + id: locals.user._id.toString(), + username: locals.user.username, + avatarUrl: locals.user.avatarUrl, + email: locals.user.email, + isAdmin: locals.user.isAdmin ?? false, + isEarlyAccess: locals.user.isEarlyAccess ?? false, + } + : null; + }) + .get("/settings", async ({ locals }) => { + const settings = await collections.settings.findOne(authCondition(locals)); + + if (settings && !validateModel(models).safeParse(settings?.activeModel).success) { + settings.activeModel = defaultModel.id; + await collections.settings.updateOne(authCondition(locals), { + $set: { activeModel: defaultModel.id }, + }); + } + + // if the model is unlisted, set the active model to the default model + if ( + settings?.activeModel && + models.find((m) => m.id === settings?.activeModel)?.unlisted === true + ) { + settings.activeModel = defaultModel.id; + await collections.settings.updateOne(authCondition(locals), { + $set: { activeModel: defaultModel.id }, + }); + } + + // todo: get user settings + return { + welcomeModalSeen: !!settings?.welcomeModalSeenAt, + welcomeModalSeenAt: settings?.welcomeModalSeenAt ?? null, + + activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel, + disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream, + directPaste: settings?.directPaste ?? DEFAULT_SETTINGS.directPaste, + hidePromptExamples: settings?.hidePromptExamples ?? DEFAULT_SETTINGS.hidePromptExamples, + shareConversationsWithModelAuthors: + settings?.shareConversationsWithModelAuthors ?? + DEFAULT_SETTINGS.shareConversationsWithModelAuthors, + + customPrompts: settings?.customPrompts ?? {}, + multimodalOverrides: settings?.multimodalOverrides ?? {}, + }; + }) + .post("/settings", async ({ locals, request }) => { + const body = await request.json(); + + const { welcomeModalSeen, ...settings } = z + .object({ + shareConversationsWithModelAuthors: z + .boolean() + .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors), + welcomeModalSeen: z.boolean().optional(), + activeModel: z.string().default(DEFAULT_SETTINGS.activeModel), + customPrompts: z.record(z.string()).default({}), + multimodalOverrides: z.record(z.boolean()).default({}), + disableStream: z.boolean().default(false), + directPaste: z.boolean().default(false), + hidePromptExamples: z.record(z.boolean()).default({}), + }) + .parse(body) satisfies SettingsEditable; + + // Tools removed: ignore tools updates + + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + ...settings, + ...(welcomeModalSeen && { welcomeModalSeenAt: new Date() }), + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + // return ok response + return new Response(); + }) + .get("/reports", async ({ locals }) => { + if (!locals.user || !locals.sessionId) { + return []; + } + + const reports = await collections.reports + .find({ + createdBy: locals.user?._id ?? locals.sessionId, + }) + .toArray(); + return reports; + }); + }); diff --git a/src/lib/server/apiToken.ts b/src/lib/server/apiToken.ts new file mode 100644 index 0000000000000000000000000000000000000000..72fa4311daf6fc97aba8d0c682477fef255b5ebd --- /dev/null +++ b/src/lib/server/apiToken.ts @@ -0,0 +1,11 @@ +import { config } from "$lib/server/config"; + +export function getApiToken(locals: App.Locals | undefined) { + if (config.USE_USER_TOKEN === "true") { + if (!locals?.token) { + throw new Error("User token not found"); + } + return locals.token; + } + return config.OPENAI_API_KEY || config.HF_TOKEN; +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..fabf4b998c1634189d01c786d17cb246884a3e71 --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,558 @@ +import { + Issuer, + type BaseClient, + type UserinfoResponse, + type TokenSet, + custom, +} from "openid-client"; +import { addHours, addWeeks, differenceInMinutes, subMinutes } from "date-fns"; +import { config } from "$lib/server/config"; +import { sha256 } from "$lib/utils/sha256"; +import { z } from "zod"; +import { dev } from "$app/environment"; +import { redirect, type Cookies } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import JSON5 from "json5"; +import { logger } from "$lib/server/logger"; +import { ObjectId } from "mongodb"; +import type { Cookie } from "elysia"; +import { adminTokenManager } from "./adminToken"; +import type { User } from "$lib/types/User"; +import type { Session } from "$lib/types/Session"; +import { base } from "$app/paths"; +import { acquireLock, isDBLocked, releaseLock } from "$lib/migrations/lock"; +import { Semaphores } from "$lib/types/Semaphore"; + +export interface OIDCSettings { + redirectURI: string; +} + +export interface OIDCUserInfo { + token: TokenSet; + userData: UserinfoResponse; +} + +const stringWithDefault = (value: string) => + z + .string() + .default(value) + .transform((el) => (el ? el : value)); + +export const OIDConfig = z + .object({ + CLIENT_ID: stringWithDefault(config.OPENID_CLIENT_ID), + CLIENT_SECRET: stringWithDefault(config.OPENID_CLIENT_SECRET), + PROVIDER_URL: stringWithDefault(config.OPENID_PROVIDER_URL), + SCOPES: stringWithDefault(config.OPENID_SCOPES), + NAME_CLAIM: stringWithDefault(config.OPENID_NAME_CLAIM).refine( + (el) => !["preferred_username", "email", "picture", "sub"].includes(el), + { message: "nameClaim cannot be one of the restricted keys." } + ), + TOLERANCE: stringWithDefault(config.OPENID_TOLERANCE), + RESOURCE: stringWithDefault(config.OPENID_RESOURCE), + ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(), + }) + .parse(JSON5.parse(config.OPENID_CONFIG || "{}")); + +export const loginEnabled = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET; + +const sameSite = z + .enum(["lax", "none", "strict"]) + .default(dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none") + .parse(config.COOKIE_SAMESITE === "" ? undefined : config.COOKIE_SAMESITE); + +const secure = z + .boolean() + .default(!(dev || config.ALLOW_INSECURE_COOKIES === "true")) + .parse(config.COOKIE_SECURE === "" ? undefined : config.COOKIE_SECURE === "true"); + +function sanitizeReturnPath(path: string | undefined | null): string | undefined { + if (!path) { + return undefined; + } + if (path.startsWith("//")) { + return undefined; + } + if (!path.startsWith("/")) { + return undefined; + } + return path; +} + +export function refreshSessionCookie(cookies: Cookies, sessionId: string) { + cookies.set(config.COOKIE_NAME, sessionId, { + path: "/", + // So that it works inside the space's iframe + sameSite, + secure, + httpOnly: true, + expires: addWeeks(new Date(), 2), + }); +} + +export async function findUser( + sessionId: string, + coupledCookieHash: string | undefined, + url: URL +): Promise<{ + user: User | null; + invalidateSession: boolean; + oauth?: Session["oauth"]; +}> { + const session = await collections.sessions.findOne({ sessionId }); + + if (!session) { + return { user: null, invalidateSession: false }; + } + + if (coupledCookieHash && session.coupledCookieHash !== coupledCookieHash) { + return { user: null, invalidateSession: true }; + } + + // Check if OAuth token needs refresh + if (session.oauth?.token && session.oauth.refreshToken) { + // If token expires in less than 5 minutes, refresh it + if (differenceInMinutes(session.oauth.token.expiresAt, new Date()) < 5) { + const lockKey = `${Semaphores.OAUTH_TOKEN_REFRESH}:${sessionId}`; + + // Acquire lock for token refresh + const lockId = await acquireLock(lockKey); + if (lockId) { + try { + // Attempt to refresh the token + const newTokenSet = await refreshOAuthToken( + { redirectURI: `${config.PUBLIC_ORIGIN}${base}/login/callback` }, + session.oauth.refreshToken, + url + ); + + if (!newTokenSet || !newTokenSet.access_token) { + // Token refresh failed, invalidate session + return { user: null, invalidateSession: true }; + } + + // Update session with new token information + const updatedOAuth = tokenSetToSessionOauth(newTokenSet); + + if (!updatedOAuth) { + // Token refresh failed, invalidate session + return { user: null, invalidateSession: true }; + } + + await collections.sessions.updateOne( + { sessionId }, + { + $set: { + oauth: updatedOAuth, + updatedAt: new Date(), + }, + } + ); + + session.oauth = updatedOAuth; + } catch (err) { + logger.error("Error during token refresh:", err); + return { user: null, invalidateSession: true }; + } finally { + await releaseLock(lockKey, lockId); + } + } else if (new Date() > session.oauth.token.expiresAt) { + // If the token has expired, we need to wait for the token refresh to complete + let attempts = 0; + do { + await new Promise((resolve) => setTimeout(resolve, 200)); + attempts++; + if (attempts > 20) { + return { user: null, invalidateSession: true }; + } + } while (await isDBLocked(lockKey)); + + const updatedSession = await collections.sessions.findOne({ sessionId }); + if (!updatedSession || updatedSession.oauth?.token === session.oauth.token) { + return { user: null, invalidateSession: true }; + } + + session.oauth = updatedSession.oauth; + } + } + } + + return { + user: await collections.users.findOne({ _id: session.userId }), + invalidateSession: false, + oauth: session.oauth, + }; +} +export const authCondition = (locals: App.Locals) => { + if (!locals.user && !locals.sessionId) { + throw new Error("User or sessionId is required"); + } + + return locals.user + ? { userId: locals.user._id } + : { sessionId: locals.sessionId, userId: { $exists: false } }; +}; + +export function tokenSetToSessionOauth(tokenSet: TokenSet): Session["oauth"] { + if (!tokenSet.access_token) { + return undefined; + } + + return { + token: { + value: tokenSet.access_token, + expiresAt: tokenSet.expires_at + ? subMinutes(new Date(tokenSet.expires_at * 1000), 1) + : addWeeks(new Date(), 2), + }, + refreshToken: tokenSet.refresh_token || undefined, + }; +} + +/** + * Generates a CSRF token using the user sessionId. Note that we don't need a secret because sessionId is enough. + */ +export async function generateCsrfToken( + sessionId: string, + redirectUrl: string, + next?: string +): Promise { + const sanitizedNext = sanitizeReturnPath(next); + const data = { + expiration: addHours(new Date(), 1).getTime(), + redirectUrl, + ...(sanitizedNext ? { next: sanitizedNext } : {}), + } as { + expiration: number; + redirectUrl: string; + next?: string; + }; + + return Buffer.from( + JSON.stringify({ + data, + signature: await sha256(JSON.stringify(data) + "##" + sessionId), + }) + ).toString("base64"); +} + +let lastIssuer: Issuer | null = null; +let lastIssuerFetchedAt: Date | null = null; +async function getOIDCClient(settings: OIDCSettings, url: URL): Promise { + if ( + lastIssuer && + lastIssuerFetchedAt && + differenceInMinutes(new Date(), lastIssuerFetchedAt) >= 10 + ) { + lastIssuer = null; + lastIssuerFetchedAt = null; + } + if (!lastIssuer) { + lastIssuer = await Issuer.discover(OIDConfig.PROVIDER_URL); + lastIssuerFetchedAt = new Date(); + } + + const issuer = lastIssuer; + + const client_config: ConstructorParameters[0] = { + client_id: OIDConfig.CLIENT_ID, + client_secret: OIDConfig.CLIENT_SECRET, + redirect_uris: [settings.redirectURI], + response_types: ["code"], + [custom.clock_tolerance]: OIDConfig.TOLERANCE || undefined, + id_token_signed_response_alg: OIDConfig.ID_TOKEN_SIGNED_RESPONSE_ALG || undefined, + }; + + if (OIDConfig.CLIENT_ID === "__CIMD__") { + OIDConfig.CLIENT_ID = new URL( + "/.well-known/oauth-cimd", + config.PUBLIC_ORIGIN || url.origin + ).toString(); + } + + const alg_supported = issuer.metadata["id_token_signing_alg_values_supported"]; + + if (Array.isArray(alg_supported)) { + client_config.id_token_signed_response_alg ??= alg_supported[0]; + } + + return new issuer.Client(client_config); +} + +export async function getOIDCAuthorizationUrl( + settings: OIDCSettings, + params: { sessionId: string; next?: string; url: URL } +): Promise { + const client = await getOIDCClient(settings, params.url); + const csrfToken = await generateCsrfToken( + params.sessionId, + settings.redirectURI, + sanitizeReturnPath(params.next) + ); + + return client.authorizationUrl({ + scope: OIDConfig.SCOPES, + state: csrfToken, + resource: OIDConfig.RESOURCE || undefined, + }); +} + +export async function getOIDCUserData( + settings: OIDCSettings, + code: string, + iss: string | undefined, + url: URL +): Promise { + const client = await getOIDCClient(settings, url); + const token = await client.callback(settings.redirectURI, { code, iss }); + const userData = await client.userinfo(token); + + return { token, userData }; +} + +/** + * Refreshes an OAuth token using the refresh token + */ +export async function refreshOAuthToken( + settings: OIDCSettings, + refreshToken: string, + url: URL +): Promise { + const client = await getOIDCClient(settings, url); + const tokenSet = await client.refresh(refreshToken); + return tokenSet; +} + +export async function validateAndParseCsrfToken( + token: string, + sessionId: string +): Promise<{ + /** This is the redirect url that was passed to the OIDC provider */ + redirectUrl: string; + /** Relative path (within this app) to return to after login */ + next?: string; +} | null> { + try { + const { data, signature } = z + .object({ + data: z.object({ + expiration: z.number().int(), + redirectUrl: z.string().url(), + next: z.string().optional(), + }), + signature: z.string().length(64), + }) + .parse(JSON.parse(token)); + + const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId); + + if (data.expiration > Date.now() && signature === reconstructSign) { + return { redirectUrl: data.redirectUrl, next: sanitizeReturnPath(data.next) }; + } + } catch (e) { + logger.error(e); + } + return null; +} + +type CookieRecord = + | { type: "elysia"; value: Record> } + | { type: "svelte"; value: Cookies }; +type HeaderRecord = + | { type: "elysia"; value: Record } + | { type: "svelte"; value: Headers }; + +export async function getCoupledCookieHash(cookie: CookieRecord): Promise { + if (!config.COUPLE_SESSION_WITH_COOKIE_NAME) { + return undefined; + } + + const cookieValue = + cookie.type === "elysia" + ? cookie.value[config.COUPLE_SESSION_WITH_COOKIE_NAME]?.value + : cookie.value.get(config.COUPLE_SESSION_WITH_COOKIE_NAME); + + if (!cookieValue) { + return "no-cookie"; + } + + return await sha256(cookieValue); +} + +export async function authenticateRequest( + headers: HeaderRecord, + cookie: CookieRecord, + url: URL, + isApi?: boolean +): Promise { + // once the entire API has been moved to elysia + // we can move this function to authPlugin.ts + // and get rid of the isApi && type: "svelte" options + const token = + cookie.type === "elysia" + ? cookie.value[config.COOKIE_NAME].value + : cookie.value.get(config.COOKIE_NAME); + + let email = null; + if (config.TRUSTED_EMAIL_HEADER) { + if (headers.type === "elysia") { + email = headers.value[config.TRUSTED_EMAIL_HEADER]; + } else { + email = headers.value.get(config.TRUSTED_EMAIL_HEADER); + } + } + + let secretSessionId: string | null = null; + let sessionId: string | null = null; + + if (email) { + secretSessionId = sessionId = await sha256(email); + return { + user: { + _id: new ObjectId(sessionId.slice(0, 24)), + name: email, + email, + createdAt: new Date(), + updatedAt: new Date(), + hfUserId: email, + avatarUrl: "", + }, + sessionId, + secretSessionId, + isAdmin: adminTokenManager.isAdmin(sessionId), + }; + } + + if (token) { + secretSessionId = token; + sessionId = await sha256(token); + + const result = await findUser(sessionId, await getCoupledCookieHash(cookie), url); + + if (result.invalidateSession) { + secretSessionId = crypto.randomUUID(); + sessionId = await sha256(secretSessionId); + + if (await collections.sessions.findOne({ sessionId })) { + throw new Error("Session ID collision"); + } + } + + return { + user: result.user ?? undefined, + token: result.oauth?.token?.value, + sessionId, + secretSessionId, + isAdmin: result.user?.isAdmin || adminTokenManager.isAdmin(sessionId), + }; + } + + if (isApi) { + const authorization = + headers.type === "elysia" + ? headers.value["Authorization"] + : headers.value.get("Authorization"); + if (authorization?.startsWith("Bearer ")) { + const token = authorization.slice(7); + const hash = await sha256(token); + sessionId = secretSessionId = hash; + + const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash }); + if (cacheHit) { + const user = await collections.users.findOne({ hfUserId: cacheHit.userId }); + if (!user) { + throw new Error("User not found"); + } + return { + user, + sessionId, + token, + secretSessionId, + isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId), + }; + } + + const response = await fetch("https://huggingface.co/api/whoami-v2", { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error("Unauthorized"); + } + + const data = await response.json(); + const user = await collections.users.findOne({ hfUserId: data.id }); + if (!user) { + throw new Error("User not found"); + } + + await collections.tokenCaches.insertOne({ + tokenHash: hash, + userId: data.id, + createdAt: new Date(), + updatedAt: new Date(), + }); + + return { + user, + sessionId, + secretSessionId, + token, + isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId), + }; + } + } + + // Generate new session if none exists + secretSessionId = crypto.randomUUID(); + sessionId = await sha256(secretSessionId); + + if (await collections.sessions.findOne({ sessionId })) { + throw new Error("Session ID collision"); + } + + return { user: undefined, sessionId, secretSessionId, isAdmin: false }; +} + +export async function triggerOauthFlow({ + url, + locals, +}: { + request: Request; + url: URL; + locals: App.Locals; +}): Promise { + // const referer = request.headers.get("referer"); + // let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`; + let redirectURI = `${url.origin}${base}/login/callback`; + + // TODO: Handle errors if provider is not responding + + if (url.searchParams.has("callback")) { + const callback = url.searchParams.get("callback") || redirectURI; + if (config.ALTERNATIVE_REDIRECT_URLS.includes(callback)) { + redirectURI = callback; + } + } + + // Preserve a safe in-app return path after login. + // Priority: explicit ?next=... (must be an absolute path), else the current path (when auto-login kicks in). + let next: string | undefined = undefined; + const nextParam = sanitizeReturnPath(url.searchParams.get("next")); + if (nextParam) { + // Only accept absolute in-app paths to prevent open redirects + next = nextParam; + } else if (!url.pathname.startsWith(`${base}/login`)) { + // For automatic login on protected pages, return to the page the user was on + next = sanitizeReturnPath(`${url.pathname}${url.search}`) ?? `${base}/`; + } else { + next = sanitizeReturnPath(`${base}/`) ?? "/"; + } + + const authorizationUrl = await getOIDCAuthorizationUrl( + { redirectURI }, + { sessionId: locals.sessionId, next, url } + ); + + throw redirect(302, authorizationUrl); +} diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ca6d522629f2177298c49a778d936c4bf39178d --- /dev/null +++ b/src/lib/server/config.ts @@ -0,0 +1,183 @@ +import { env as publicEnv } from "$env/dynamic/public"; +import { env as serverEnv } from "$env/dynamic/private"; +import { building } from "$app/environment"; +import type { Collection } from "mongodb"; +import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey"; +import type { Semaphore } from "$lib/types/Semaphore"; +import { Semaphores } from "$lib/types/Semaphore"; + +export type PublicConfigKey = keyof typeof publicEnv; +const keysFromEnv = { ...publicEnv, ...serverEnv }; +export type ConfigKey = keyof typeof keysFromEnv; + +class ConfigManager { + private keysFromDB: Partial> = {}; + private isInitialized = false; + + private configCollection: Collection | undefined; + private semaphoreCollection: Collection | undefined; + private lastConfigUpdate: Date | undefined; + + async init() { + if (this.isInitialized) return; + + if (import.meta.env.MODE === "test") { + this.isInitialized = true; + return; + } + + const { getCollectionsEarly } = await import("./database"); + const collections = await getCollectionsEarly(); + + this.configCollection = collections.config; + this.semaphoreCollection = collections.semaphores; + + await this.checkForUpdates().then(() => { + this.isInitialized = true; + }); + } + + get ConfigManagerEnabled() { + return serverEnv.ENABLE_CONFIG_MANAGER === "true" && import.meta.env.MODE !== "test"; + } + + get isHuggingChat() { + return this.get("PUBLIC_APP_ASSETS") === "huggingchat"; + } + + async checkForUpdates() { + if (await this.isConfigStale()) { + await this.updateConfig(); + } + } + + async isConfigStale(): Promise { + if (!this.lastConfigUpdate || !this.isInitialized) { + return true; + } + const count = await this.semaphoreCollection?.countDocuments({ + key: Semaphores.CONFIG_UPDATE, + updatedAt: { $gt: this.lastConfigUpdate }, + }); + return count !== undefined && count > 0; + } + + async updateConfig() { + const configs = (await this.configCollection?.find({}).toArray()) ?? []; + this.keysFromDB = configs.reduce( + (acc, curr) => { + acc[curr.key as ConfigKey] = curr.value; + return acc; + }, + {} as Record + ); + + this.lastConfigUpdate = new Date(); + } + + get(key: ConfigKey): string { + if (!this.ConfigManagerEnabled) { + return keysFromEnv[key] || ""; + } + return this.keysFromDB[key] || keysFromEnv[key] || ""; + } + + async updateSemaphore() { + await this.semaphoreCollection?.updateOne( + { key: Semaphores.CONFIG_UPDATE }, + { + $set: { + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { upsert: true } + ); + } + + async set(key: ConfigKey, value: string) { + if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); + await this.configCollection?.updateOne({ key }, { $set: { value } }, { upsert: true }); + this.keysFromDB[key] = value; + await this.updateSemaphore(); + } + + async delete(key: ConfigKey) { + if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); + await this.configCollection?.deleteOne({ key }); + delete this.keysFromDB[key]; + await this.updateSemaphore(); + } + + async clear() { + if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); + await this.configCollection?.deleteMany({}); + this.keysFromDB = {}; + await this.updateSemaphore(); + } + + getPublicConfig() { + let config = { + ...Object.fromEntries( + Object.entries(keysFromEnv).filter(([key]) => key.startsWith("PUBLIC_")) + ), + } as Record; + + if (this.ConfigManagerEnabled) { + config = { + ...config, + ...Object.fromEntries( + Object.entries(this.keysFromDB).filter(([key]) => key.startsWith("PUBLIC_")) + ), + }; + } + + const publicEnvKeys = Object.keys(publicEnv); + + return Object.fromEntries( + Object.entries(config).filter(([key]) => publicEnvKeys.includes(key)) + ) as Record; + } +} + +// Create the instance and initialize it. +const configManager = new ConfigManager(); + +export const ready = (async () => { + if (!building) { + await configManager.init(); + } +})(); + +type ExtraConfigKeys = + | "HF_TOKEN" + | "OLD_MODELS" + | "ENABLE_ASSISTANTS" + | "METRICS_ENABLED" + | "METRICS_PORT"; + +type ConfigProxy = ConfigManager & { [K in ConfigKey | ExtraConfigKeys]: string }; + +export const config: ConfigProxy = new Proxy(configManager, { + get(target, prop, receiver) { + if (prop in target) { + return Reflect.get(target, prop, receiver); + } + if (typeof prop === "string") { + return target.get(prop as ConfigKey); + } + return undefined; + }, + set(target, prop, value, receiver) { + if (prop in target) { + return Reflect.set(target, prop, value, receiver); + } + if (typeof prop === "string") { + target.set(prop as ConfigKey, value); + return true; + } + return false; + }, +}) as ConfigProxy; diff --git a/src/lib/server/conversation.ts b/src/lib/server/conversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..cbe46f3ca048613a8efca5c6dd1511e95fff8fc2 --- /dev/null +++ b/src/lib/server/conversation.ts @@ -0,0 +1,83 @@ +import { collections } from "$lib/server/database"; +import { MetricsServer } from "$lib/server/metrics"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { authCondition } from "$lib/server/auth"; + +/** + * Create a new conversation from a shared conversation ID. + * If the conversation already exists for the user/session, return the existing conversation ID. + * returns the conversation ID. + */ +export async function createConversationFromShare( + fromShareId: string, + locals: App.Locals, + userAgent?: string +): Promise { + const conversation = await collections.sharedConversations.findOne({ + _id: fromShareId, + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + // Check if shared conversation exists already for this user/session + const existingConversation = await collections.conversations.findOne({ + "meta.fromShareId": fromShareId, + ...authCondition(locals), + }); + + if (existingConversation) { + return existingConversation._id.toString(); + } + + // Create new conversation from shared conversation + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + title: conversation.title.replace(/<\/?think>/gi, "").trim(), + rootMessageId: conversation.rootMessageId, + messages: conversation.messages, + model: conversation.model, + preprompt: conversation.preprompt, + createdAt: new Date(), + updatedAt: new Date(), + userAgent, + ...(locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId }), + meta: { fromShareId }, + }); + + // Copy files from shared conversation bucket entries to the new conversation + // Shared files are stored with filenames "${sharedId}-${sha}" and metadata.conversation = sharedId + // New conversation expects files to be stored under its own id prefix + const newConvId = res.insertedId.toString(); + const sharedId = fromShareId; + const files = await collections.bucket.find({ filename: { $regex: `^${sharedId}-` } }).toArray(); + + await Promise.all( + files.map( + (file) => + new Promise((resolve, reject) => { + try { + const newFilename = file.filename.replace(`${sharedId}-`, `${newConvId}-`); + const downloadStream = collections.bucket.openDownloadStream(file._id); + const uploadStream = collections.bucket.openUploadStream(newFilename, { + metadata: { ...file.metadata, conversation: newConvId }, + }); + downloadStream + .on("error", reject) + .pipe(uploadStream) + .on("error", reject) + .on("finish", () => resolve()); + } catch (e) { + reject(e); + } + }) + ) + ); + + if (MetricsServer.isEnabled()) { + MetricsServer.getMetrics().model.conversationsTotal.inc({ model: conversation.model }); + } + return res.insertedId.toString(); +} diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts new file mode 100644 index 0000000000000000000000000000000000000000..29a61fbad4b7d36e841da330450555d9b0bdfe7e --- /dev/null +++ b/src/lib/server/database.ts @@ -0,0 +1,313 @@ +import { GridFSBucket, MongoClient } from "mongodb"; +import type { Conversation } from "$lib/types/Conversation"; +import type { SharedConversation } from "$lib/types/SharedConversation"; +import type { AbortedGeneration } from "$lib/types/AbortedGeneration"; +import type { Settings } from "$lib/types/Settings"; +import type { User } from "$lib/types/User"; +import type { MessageEvent } from "$lib/types/MessageEvent"; +import type { Session } from "$lib/types/Session"; +import type { Assistant } from "$lib/types/Assistant"; +import type { Report } from "$lib/types/Report"; +import type { ConversationStats } from "$lib/types/ConversationStats"; +import type { MigrationResult } from "$lib/types/MigrationResult"; +import type { Semaphore } from "$lib/types/Semaphore"; +import type { AssistantStats } from "$lib/types/AssistantStats"; +import { MongoMemoryServer } from "mongodb-memory-server"; +import { logger } from "$lib/server/logger"; +import { building } from "$app/environment"; +import type { TokenCache } from "$lib/types/TokenCache"; +import { onExit } from "./exitHandler"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { existsSync, mkdirSync } from "fs"; +import { findRepoRoot } from "./findRepoRoot"; +import type { ConfigKey } from "$lib/types/ConfigKey"; +import { config } from "$lib/server/config"; + +export const CONVERSATION_STATS_COLLECTION = "conversations.stats"; + +export class Database { + private client?: MongoClient; + private mongoServer?: MongoMemoryServer; + + private static instance: Database; + + private async init() { + const DB_FOLDER = + config.MONGO_STORAGE_PATH || + join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "db"); + + if (!config.MONGODB_URL) { + logger.warn("No MongoDB URL found, using in-memory server"); + + logger.info(`Using database path: ${DB_FOLDER}`); + // Create db directory if it doesn't exist + if (!existsSync(DB_FOLDER)) { + logger.info(`Creating database directory at ${DB_FOLDER}`); + mkdirSync(DB_FOLDER, { recursive: true }); + } + + this.mongoServer = await MongoMemoryServer.create({ + instance: { + dbName: config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""), + dbPath: DB_FOLDER, + }, + binary: { + version: "7.0.18", + }, + }); + this.client = new MongoClient(this.mongoServer.getUri(), { + directConnection: config.MONGODB_DIRECT_CONNECTION === "true", + }); + } else { + this.client = new MongoClient(config.MONGODB_URL, { + directConnection: config.MONGODB_DIRECT_CONNECTION === "true", + }); + } + + try { + logger.info("Connecting to database"); + await this.client.connect(); + logger.info("Connected to database"); + this.client.db(config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); + await this.initDatabase(); + } catch (err) { + logger.error(err, "Connection error"); + process.exit(1); + } + + // Disconnect DB on exit + onExit(async () => { + logger.info("Closing database connection"); + await this.client?.close(true); + await this.mongoServer?.stop(); + }); + } + + public static async getInstance(): Promise { + if (!Database.instance) { + Database.instance = new Database(); + await Database.instance.init(); + } + + return Database.instance; + } + + /** + * Return mongoClient + */ + public getClient(): MongoClient { + if (!this.client) { + throw new Error("Database not initialized"); + } + + return this.client; + } + + /** + * Return map of database's collections + */ + public getCollections() { + if (!this.client) { + throw new Error("Database not initialized"); + } + + const db = this.client.db( + config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "") + ); + + const conversations = db.collection("conversations"); + const conversationStats = db.collection(CONVERSATION_STATS_COLLECTION); + const assistants = db.collection("assistants"); + const assistantStats = db.collection("assistants.stats"); + const reports = db.collection("reports"); + const sharedConversations = db.collection("sharedConversations"); + const abortedGenerations = db.collection("abortedGenerations"); + const settings = db.collection("settings"); + const users = db.collection("users"); + const sessions = db.collection("sessions"); + const messageEvents = db.collection("messageEvents"); + const bucket = new GridFSBucket(db, { bucketName: "files" }); + const migrationResults = db.collection("migrationResults"); + const semaphores = db.collection("semaphores"); + const tokenCaches = db.collection("tokens"); + const tools = db.collection("tools"); + const configCollection = db.collection("config"); + + return { + conversations, + conversationStats, + assistants, + assistantStats, + reports, + sharedConversations, + abortedGenerations, + settings, + users, + sessions, + messageEvents, + bucket, + migrationResults, + semaphores, + tokenCaches, + tools, + config: configCollection, + }; + } + + /** + * Init database once connected: Index creation + * @private + */ + private initDatabase() { + const { + conversations, + conversationStats, + assistants, + assistantStats, + reports, + sharedConversations, + abortedGenerations, + settings, + users, + sessions, + messageEvents, + semaphores, + tokenCaches, + config, + } = this.getCollections(); + + conversations + .createIndex( + { sessionId: 1, updatedAt: -1 }, + { partialFilterExpression: { sessionId: { $exists: true } } } + ) + .catch((e) => logger.error(e)); + conversations + .createIndex( + { userId: 1, updatedAt: -1 }, + { partialFilterExpression: { userId: { $exists: true } } } + ) + .catch((e) => logger.error(e)); + conversations + .createIndex( + { "message.id": 1, "message.ancestors": 1 }, + { partialFilterExpression: { userId: { $exists: true } } } + ) + .catch((e) => logger.error(e)); + // Not strictly necessary, could use _id, but more convenient. Also for stats + // To do stats on conversation messages + conversations + .createIndex({ "messages.createdAt": 1 }, { sparse: true }) + .catch((e) => logger.error(e)); + // Unique index for stats + conversationStats + .createIndex( + { + type: 1, + "date.field": 1, + "date.span": 1, + "date.at": 1, + distinct: 1, + }, + { unique: true } + ) + .catch((e) => logger.error(e)); + // Allow easy check of last computed stat for given type/dateField + conversationStats + .createIndex({ + type: 1, + "date.field": 1, + "date.at": 1, + }) + .catch((e) => logger.error(e)); + abortedGenerations + .createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }) + .catch((e) => logger.error(e)); + abortedGenerations + .createIndex({ conversationId: 1 }, { unique: true }) + .catch((e) => logger.error(e)); + sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch((e) => logger.error(e)); + settings + .createIndex({ sessionId: 1 }, { unique: true, sparse: true }) + .catch((e) => logger.error(e)); + settings + .createIndex({ userId: 1 }, { unique: true, sparse: true }) + .catch((e) => logger.error(e)); + settings.createIndex({ assistants: 1 }).catch((e) => logger.error(e)); + users.createIndex({ hfUserId: 1 }, { unique: true }).catch((e) => logger.error(e)); + users + .createIndex({ sessionId: 1 }, { unique: true, sparse: true }) + .catch((e) => logger.error(e)); + // No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users + users.createIndex({ username: 1 }).catch((e) => logger.error(e)); + messageEvents + .createIndex({ expiresAt: 1 }, { expireAfterSeconds: 1 }) + .catch((e) => logger.error(e)); + sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch((e) => logger.error(e)); + sessions.createIndex({ sessionId: 1 }, { unique: true }).catch((e) => logger.error(e)); + assistants.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e)); + assistants.createIndex({ userCount: 1 }).catch((e) => logger.error(e)); + assistants.createIndex({ review: 1, userCount: -1 }).catch((e) => logger.error(e)); + assistants.createIndex({ modelId: 1, userCount: -1 }).catch((e) => logger.error(e)); + assistants.createIndex({ searchTokens: 1 }).catch((e) => logger.error(e)); + assistants.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e)); + assistants + .createIndex({ last24HoursUseCount: -1, useCount: -1, _id: 1 }) + .catch((e) => logger.error(e)); + assistantStats + // Order of keys is important for the queries + .createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true }) + .catch((e) => logger.error(e)); + reports.createIndex({ assistantId: 1 }).catch((e) => logger.error(e)); + reports.createIndex({ createdBy: 1, assistantId: 1 }).catch((e) => logger.error(e)); + + // Unique index for semaphore and migration results + semaphores.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e)); + semaphores + .createIndex({ deleteAt: 1 }, { expireAfterSeconds: 1 }) + .catch((e) => logger.error(e)); + tokenCaches + .createIndex({ createdAt: 1 }, { expireAfterSeconds: 5 * 60 }) + .catch((e) => logger.error(e)); + tokenCaches.createIndex({ tokenHash: 1 }).catch((e) => logger.error(e)); + // Tools removed: skipping tools indexes + + conversations + .createIndex({ + "messages.from": 1, + createdAt: 1, + }) + .catch((e) => logger.error(e)); + + conversations + .createIndex({ + userId: 1, + sessionId: 1, + }) + .catch((e) => logger.error(e)); + + config.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e)); + } +} + +export let collections: ReturnType; + +export const ready = (async () => { + if (!building) { + const db = await Database.getInstance(); + collections = db.getCollections(); + } else { + collections = {} as unknown as ReturnType; + } +})(); + +export async function getCollectionsEarly(): Promise< + ReturnType +> { + await ready; + if (!collections) { + throw new Error("Database not initialized"); + } + return collections; +} diff --git a/src/lib/server/endpoints/document.ts b/src/lib/server/endpoints/document.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d16d162e346306da66137f24d2a08ac4295e876 --- /dev/null +++ b/src/lib/server/endpoints/document.ts @@ -0,0 +1,68 @@ +import type { MessageFile } from "$lib/types/Message"; +import { z } from "zod"; + +export interface FileProcessorOptions { + supportedMimeTypes: TMimeType[]; + maxSizeInMB: number; +} + +// Removed unused ImageProcessor type alias + +export const createDocumentProcessorOptionsValidator = ( + defaults: FileProcessorOptions +) => { + return z + .object({ + supportedMimeTypes: z + .array( + z.enum([ + defaults.supportedMimeTypes[0], + ...defaults.supportedMimeTypes.slice(1), + ]) + ) + .default(defaults.supportedMimeTypes), + maxSizeInMB: z.number().positive().default(defaults.maxSizeInMB), + }) + .default(defaults); +}; + +// Removed unused DocumentProcessor type alias + +export type AsyncDocumentProcessor = ( + file: MessageFile +) => Promise<{ + file: Buffer; + mime: TMimeType; +}>; + +export function makeDocumentProcessor( + options: FileProcessorOptions +): AsyncDocumentProcessor { + return async (file) => { + const { supportedMimeTypes, maxSizeInMB } = options; + const { mime, value } = file; + + const buffer = Buffer.from(value, "base64"); + const tooLargeInBytes = buffer.byteLength > maxSizeInMB * 1000 * 1000; + + if (tooLargeInBytes) { + throw Error("Document is too large"); + } + + const outputMime = validateMimeType(supportedMimeTypes, mime); + return { file: buffer, mime: outputMime }; + }; +} + +const validateMimeType = ( + supportedMimes: T, + mime: string +): T[number] => { + if (!supportedMimes.includes(mime)) { + const supportedMimesStr = supportedMimes.join(", "); + + throw Error(`Mimetype "${mime}" not found in supported mimes: ${supportedMimesStr}`); + } + + return mime; +}; diff --git a/src/lib/server/endpoints/endpoints.ts b/src/lib/server/endpoints/endpoints.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6462bbb5e299521091cb48fe455b9cf5d37442a --- /dev/null +++ b/src/lib/server/endpoints/endpoints.ts @@ -0,0 +1,41 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { + TextGenerationStreamOutput, + TextGenerationStreamToken, + InferenceProvider, +} from "@huggingface/inference"; +import { z } from "zod"; +import { endpointOAIParametersSchema, endpointOai } from "./openai/endpointOai"; +import type { Model } from "$lib/types/Model"; +import type { ObjectId } from "mongodb"; + +export type EndpointMessage = Omit; + +// parameters passed when generating text +export interface EndpointParameters { + messages: EndpointMessage[]; + preprompt?: Conversation["preprompt"]; + generateSettings?: Partial; + isMultimodal?: boolean; + conversationId?: ObjectId; + locals: App.Locals | undefined; + abortSignal?: AbortSignal; +} + +export type TextGenerationStreamOutputSimplified = TextGenerationStreamOutput & { + token: TextGenerationStreamToken; + routerMetadata?: { route?: string; model?: string; provider?: InferenceProvider }; +}; +// type signature for the endpoint +export type Endpoint = ( + params: EndpointParameters +) => Promise>; + +// list of all endpoint generators +export const endpoints = { + openai: endpointOai, +}; + +export const endpointSchema = z.discriminatedUnion("type", [endpointOAIParametersSchema]); +export default endpoints; diff --git a/src/lib/server/endpoints/images.ts b/src/lib/server/endpoints/images.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d408814cf229ad64a0128a537167a747737f84c --- /dev/null +++ b/src/lib/server/endpoints/images.ts @@ -0,0 +1,211 @@ +import type { Sharp } from "sharp"; +import sharp from "sharp"; +import type { MessageFile } from "$lib/types/Message"; +import { z, type util } from "zod"; + +export interface ImageProcessorOptions { + supportedMimeTypes: TMimeType[]; + preferredMimeType: TMimeType; + maxSizeInMB: number; + maxWidth: number; + maxHeight: number; +} +export type ImageProcessor = (file: MessageFile) => Promise<{ + image: Buffer; + mime: TMimeType; +}>; + +export function createImageProcessorOptionsValidator( + defaults: ImageProcessorOptions +) { + return z + .object({ + supportedMimeTypes: z + .array( + z.enum([ + defaults.supportedMimeTypes[0], + ...defaults.supportedMimeTypes.slice(1), + ]) + ) + .default(defaults.supportedMimeTypes), + preferredMimeType: z + .enum([defaults.supportedMimeTypes[0], ...defaults.supportedMimeTypes.slice(1)]) + .default(defaults.preferredMimeType as util.noUndefined), + maxSizeInMB: z.number().positive().default(defaults.maxSizeInMB), + maxWidth: z.number().int().positive().default(defaults.maxWidth), + maxHeight: z.number().int().positive().default(defaults.maxHeight), + }) + .default(defaults); +} + +export function makeImageProcessor( + options: ImageProcessorOptions +): ImageProcessor { + return async (file) => { + const { supportedMimeTypes, preferredMimeType, maxSizeInMB, maxWidth, maxHeight } = options; + const { mime, value } = file; + + const buffer = Buffer.from(value, "base64"); + let sharpInst = sharp(buffer); + + const metadata = await sharpInst.metadata(); + if (!metadata) throw Error("Failed to read image metadata"); + const { width, height } = metadata; + if (width === undefined || height === undefined) throw Error("Failed to read image size"); + + const tooLargeInSize = width > maxWidth || height > maxHeight; + const tooLargeInBytes = buffer.byteLength > maxSizeInMB * 1000 * 1000; + + const outputMime = chooseMimeType(supportedMimeTypes, preferredMimeType, mime, { + preferSizeReduction: tooLargeInBytes, + }); + + // Resize if necessary + if (tooLargeInSize || tooLargeInBytes) { + const size = chooseImageSize({ + mime: outputMime, + width, + height, + maxWidth, + maxHeight, + maxSizeInMB, + }); + if (size.width !== width || size.height !== height) { + sharpInst = resizeImage(sharpInst, size.width, size.height); + } + } + + // Convert format if necessary + // We always want to convert the image when the file was too large in bytes + // so we can guarantee that ideal options are used, which are expected when + // choosing the image size + if (outputMime !== mime || tooLargeInBytes) { + sharpInst = convertImage(sharpInst, outputMime); + } + + const processedImage = await sharpInst.toBuffer(); + return { image: processedImage, mime: outputMime }; + }; +} + +const outputFormats = ["png", "jpeg", "webp", "avif", "tiff", "gif"] as const; +type OutputImgFormat = (typeof outputFormats)[number]; +const isOutputFormat = (format: string): format is (typeof outputFormats)[number] => + outputFormats.includes(format as OutputImgFormat); + +export function convertImage(sharpInst: Sharp, outputMime: string): Sharp { + const [type, format] = outputMime.split("/"); + if (type !== "image") throw Error(`Requested non-image mime type: ${outputMime}`); + if (!isOutputFormat(format)) { + throw Error(`Requested to convert to an unsupported format: ${format}`); + } + + return sharpInst[format](); +} + +// heic/heif requires proprietary license +// TODO: blocking heif may be incorrect considering it also supports av1, so we should instead +// detect the compression method used via sharp().metadata().compression +// TODO: consider what to do about animated formats: apng, gif, animated webp, ... +const blocklistedMimes = ["image/heic", "image/heif"]; + +/** Sorted from largest to smallest */ +const mimesBySizeDesc = [ + "image/png", + "image/tiff", + "image/gif", + "image/jpeg", + "image/webp", + "image/avif", +]; + +/** + * Defaults to preferred format or uses existing mime if supported + * When preferSizeReduction is true, it will choose the smallest format that is supported + **/ +function chooseMimeType( + supportedMimes: T, + preferredMime: string, + mime: string, + { preferSizeReduction }: { preferSizeReduction: boolean } +): T[number] { + if (!supportedMimes.includes(preferredMime)) { + const supportedMimesStr = supportedMimes.join(", "); + throw Error( + `Preferred format "${preferredMime}" not found in supported mimes: ${supportedMimesStr}` + ); + } + + const [type] = mime.split("/"); + if (type !== "image") throw Error(`Received non-image mime type: ${mime}`); + + if (supportedMimes.includes(mime) && !preferSizeReduction) return mime; + + if (blocklistedMimes.includes(mime)) throw Error(`Received blocklisted mime type: ${mime}`); + + const smallestMime = mimesBySizeDesc.findLast((m) => supportedMimes.includes(m)); + return smallestMime ?? preferredMime; +} + +interface ImageSizeOptions { + mime: string; + width: number; + height: number; + maxWidth: number; + maxHeight: number; + maxSizeInMB: number; +} + +/** Resizes the image to fit within the specified size in MB by guessing the output size */ +export function chooseImageSize({ + mime, + width, + height, + maxWidth, + maxHeight, + maxSizeInMB, +}: ImageSizeOptions): { width: number; height: number } { + const biggestDiscrepency = Math.max(1, width / maxWidth, height / maxHeight); + + let selectedWidth = Math.ceil(width / biggestDiscrepency); + let selectedHeight = Math.ceil(height / biggestDiscrepency); + + do { + const estimatedSize = estimateImageSizeInBytes(mime, selectedWidth, selectedHeight); + if (estimatedSize < maxSizeInMB * 1024 * 1024) { + return { width: selectedWidth, height: selectedHeight }; + } + selectedWidth = Math.floor(selectedWidth / 1.1); + selectedHeight = Math.floor(selectedHeight / 1.1); + } while (selectedWidth > 1 && selectedHeight > 1); + + throw Error(`Failed to resize image to fit within ${maxSizeInMB}MB`); +} + +const mimeToCompressionRatio: Record = { + "image/png": 1 / 2, + "image/jpeg": 1 / 10, + "image/webp": 1 / 4, + "image/avif": 1 / 5, + "image/tiff": 1, + "image/gif": 1 / 5, +}; + +/** + * Guesses the side of an image in MB based on its format and dimensions + * Should guess the worst case + **/ +function estimateImageSizeInBytes(mime: string, width: number, height: number): number { + const compressionRatio = mimeToCompressionRatio[mime]; + if (!compressionRatio) throw Error(`Unsupported image format: ${mime}`); + + const bitsPerPixel = 32; // Assuming 32-bit color depth for 8-bit R G B A + const bytesPerPixel = bitsPerPixel / 8; + const uncompressedSize = width * height * bytesPerPixel; + + return uncompressedSize * compressionRatio; +} + +export function resizeImage(sharpInst: Sharp, maxWidth: number, maxHeight: number): Sharp { + return sharpInst.resize({ width: maxWidth, height: maxHeight, fit: "inside" }); +} diff --git a/src/lib/server/endpoints/openai/endpointOai.ts b/src/lib/server/endpoints/openai/endpointOai.ts new file mode 100644 index 0000000000000000000000000000000000000000..259606ac9f14fcc92a17e30899a39e78f4fce6a1 --- /dev/null +++ b/src/lib/server/endpoints/openai/endpointOai.ts @@ -0,0 +1,337 @@ +import { z } from "zod"; +import { openAICompletionToTextGenerationStream } from "./openAICompletionToTextGenerationStream"; +import { + openAIChatToTextGenerationSingle, + openAIChatToTextGenerationStream, +} from "./openAIChatToTextGenerationStream"; +import type { CompletionCreateParamsStreaming } from "openai/resources/completions"; +import type { + ChatCompletionCreateParamsNonStreaming, + ChatCompletionCreateParamsStreaming, +} from "openai/resources/chat/completions"; +import { buildPrompt } from "$lib/buildPrompt"; +import { config } from "$lib/server/config"; +import type { Endpoint } from "../endpoints"; +import type OpenAI from "openai"; +import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; +import { TEXT_MIME_ALLOWLIST } from "$lib/constants/mime"; +import type { MessageFile } from "$lib/types/Message"; +import type { EndpointMessage } from "../endpoints"; +// uuid import removed (no tool call ids) + +export const endpointOAIParametersSchema = z.object({ + weight: z.number().int().positive().default(1), + model: z.any(), + type: z.literal("openai"), + baseURL: z.string().url().default("https://api.openai.com/v1"), + // Canonical auth token is OPENAI_API_KEY; keep HF_TOKEN as legacy alias + apiKey: z.string().default(config.OPENAI_API_KEY || config.HF_TOKEN || "sk-"), + completion: z + .union([z.literal("completions"), z.literal("chat_completions")]) + .default("chat_completions"), + defaultHeaders: z.record(z.string()).optional(), + defaultQuery: z.record(z.string()).optional(), + extraBody: z.record(z.any()).optional(), + multimodal: z + .object({ + image: createImageProcessorOptionsValidator({ + supportedMimeTypes: [ + // Restrict to the most widely-supported formats + "image/png", + "image/jpeg", + ], + preferredMimeType: "image/jpeg", + maxSizeInMB: 1, + maxWidth: 1024, + maxHeight: 1024, + }), + }) + .default({}), + /* enable use of max_completion_tokens in place of max_tokens */ + useCompletionTokens: z.boolean().default(false), + streamingSupported: z.boolean().default(true), +}); + +export async function endpointOai( + input: z.input +): Promise { + const { + baseURL, + apiKey, + completion, + model, + defaultHeaders, + defaultQuery, + multimodal, + extraBody, + useCompletionTokens, + streamingSupported, + } = endpointOAIParametersSchema.parse(input); + + let OpenAI; + try { + OpenAI = (await import("openai")).OpenAI; + } catch (e) { + throw new Error("Failed to import OpenAI", { cause: e }); + } + + // Store router metadata if captured + let routerMetadata: { route?: string; model?: string; provider?: string } = {}; + + // Custom fetch wrapper to capture response headers for router metadata + const customFetch = async (url: RequestInfo, init?: RequestInit): Promise => { + const response = await fetch(url, init); + + // Capture router headers if present (fallback for non-streaming) + const routeHeader = response.headers.get("X-Router-Route"); + const modelHeader = response.headers.get("X-Router-Model"); + const providerHeader = response.headers.get("x-inference-provider"); + + if (routeHeader && modelHeader) { + routerMetadata = { + route: routeHeader, + model: modelHeader, + provider: providerHeader || undefined, + }; + } else if (providerHeader) { + // Even without router metadata, capture provider info + routerMetadata = { + provider: providerHeader, + }; + } + + return response; + }; + + const openai = new OpenAI({ + apiKey: apiKey || "sk-", + baseURL, + defaultHeaders: { + ...(config.PUBLIC_APP_NAME === "HuggingChat" && { "User-Agent": "huggingchat" }), + ...defaultHeaders, + }, + defaultQuery, + fetch: customFetch, + }); + + const imageProcessor = makeImageProcessor(multimodal.image); + + if (completion === "completions") { + return async ({ + messages, + preprompt, + generateSettings, + conversationId, + locals, + abortSignal, + }) => { + const prompt = await buildPrompt({ + messages, + preprompt, + model, + }); + + const parameters = { ...model.parameters, ...generateSettings }; + const body: CompletionCreateParamsStreaming = { + model: model.id ?? model.name, + prompt, + stream: true, + max_tokens: parameters?.max_tokens, + stop: parameters?.stop, + temperature: parameters?.temperature, + top_p: parameters?.top_p, + frequency_penalty: parameters?.frequency_penalty, + presence_penalty: parameters?.presence_penalty, + }; + + const openAICompletion = await openai.completions.create(body, { + body: { ...body, ...extraBody }, + headers: { + "ChatUI-Conversation-ID": conversationId?.toString() ?? "", + "X-use-cache": "false", + ...(locals?.token ? { Authorization: `Bearer ${locals.token}` } : {}), + }, + signal: abortSignal, + }); + + return openAICompletionToTextGenerationStream(openAICompletion); + }; + } else if (completion === "chat_completions") { + return async ({ + messages, + preprompt, + generateSettings, + conversationId, + isMultimodal, + locals, + abortSignal, + }) => { + // Format messages for the chat API, handling multimodal content if supported + let messagesOpenAI: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = + await prepareMessages(messages, imageProcessor, isMultimodal ?? model.multimodal); + + // Normalize preprompt and handle empty values + const normalizedPreprompt = + typeof preprompt === "string" ? preprompt.trim() : ""; + + // Check if a system message already exists as the first message + const hasSystemMessage = + messagesOpenAI.length > 0 && messagesOpenAI[0]?.role === "system"; + + if (hasSystemMessage) { + // Prepend normalized preprompt to existing system content when non-empty + if (normalizedPreprompt) { + const userSystemPrompt = + (typeof messagesOpenAI[0].content === "string" + ? (messagesOpenAI[0].content as string) + : "") || ""; + messagesOpenAI[0].content = + normalizedPreprompt + (userSystemPrompt ? "\n\n" + userSystemPrompt : ""); + } + } else { + // Insert a system message only if the preprompt is non-empty + if (normalizedPreprompt) { + messagesOpenAI = [ + { role: "system", content: normalizedPreprompt }, + ...messagesOpenAI, + ]; + } + } + + // Combine model defaults with request-specific parameters + const parameters = { ...model.parameters, ...generateSettings }; + const body = { + model: model.id ?? model.name, + messages: messagesOpenAI, + stream: streamingSupported, + // Support two different ways of specifying token limits depending on the model + ...(useCompletionTokens + ? { max_completion_tokens: parameters?.max_tokens } + : { max_tokens: parameters?.max_tokens }), + stop: parameters?.stop, + temperature: parameters?.temperature, + top_p: parameters?.top_p, + frequency_penalty: parameters?.frequency_penalty, + presence_penalty: parameters?.presence_penalty, + }; + + // Handle both streaming and non-streaming responses with appropriate processors + if (streamingSupported) { + const openChatAICompletion = await openai.chat.completions.create( + body as ChatCompletionCreateParamsStreaming, + { + body: { ...body, ...extraBody }, + headers: { + "ChatUI-Conversation-ID": conversationId?.toString() ?? "", + "X-use-cache": "false", + ...(locals?.token ? { Authorization: `Bearer ${locals.token}` } : {}), + }, + signal: abortSignal, + } + ); + return openAIChatToTextGenerationStream(openChatAICompletion, () => routerMetadata); + } else { + const openChatAICompletion = await openai.chat.completions.create( + body as ChatCompletionCreateParamsNonStreaming, + { + body: { ...body, ...extraBody }, + headers: { + "ChatUI-Conversation-ID": conversationId?.toString() ?? "", + "X-use-cache": "false", + ...(locals?.token ? { Authorization: `Bearer ${locals.token}` } : {}), + }, + signal: abortSignal, + } + ); + return openAIChatToTextGenerationSingle(openChatAICompletion, () => routerMetadata); + } + }; + } else { + throw new Error("Invalid completion type"); + } +} + +async function prepareMessages( + messages: EndpointMessage[], + imageProcessor: ReturnType, + isMultimodal: boolean +): Promise { + return Promise.all( + messages.map(async (message) => { + if (message.from === "user" && message.files && message.files.length > 0) { + const { imageParts, textContent } = await prepareFiles( + imageProcessor, + message.files, + isMultimodal + ); + + // If we have text files, prepend their content to the message + let messageText = message.content; + if (textContent.length > 0) { + messageText = textContent + "\n\n" + message.content; + } + + // If we have images and multimodal is enabled, use structured content + if (imageParts.length > 0 && isMultimodal) { + const parts = [{ type: "text" as const, text: messageText }, ...imageParts]; + return { role: message.from, content: parts }; + } + + // Otherwise just use the text (possibly with injected file content) + return { role: message.from, content: messageText }; + } + return { role: message.from, content: message.content }; + }) + ); +} + +async function prepareFiles( + imageProcessor: ReturnType, + files: MessageFile[], + isMultimodal: boolean +): Promise<{ + imageParts: OpenAI.Chat.Completions.ChatCompletionContentPartImage[]; + textContent: string; +}> { + // Separate image and text files + const imageFiles = files.filter((file) => file.mime.startsWith("image/")); + const textFiles = files.filter((file) => { + const mime = (file.mime || "").toLowerCase(); + const [fileType, fileSubtype] = mime.split("/"); + return TEXT_MIME_ALLOWLIST.some((allowed) => { + const [type, subtype] = allowed.toLowerCase().split("/"); + const typeOk = type === "*" || type === fileType; + const subOk = subtype === "*" || subtype === fileSubtype; + return typeOk && subOk; + }); + }); + + // Process images if multimodal is enabled + let imageParts: OpenAI.Chat.Completions.ChatCompletionContentPartImage[] = []; + if (isMultimodal && imageFiles.length > 0) { + const processedFiles = await Promise.all(imageFiles.map(imageProcessor)); + imageParts = processedFiles.map((file) => ({ + type: "image_url" as const, + image_url: { + url: `data:${file.mime};base64,${file.image.toString("base64")}`, + // Improves compatibility with some OpenAI-compatible servers + // that expect an explicit detail setting. + detail: "auto", + }, + })); + } + + // Process text files - inject their content + let textContent = ""; + if (textFiles.length > 0) { + const textParts = await Promise.all( + textFiles.map(async (file) => { + const content = Buffer.from(file.value, "base64").toString("utf-8"); + return `\n${content}\n`; + }) + ); + textContent = textParts.join("\n\n"); + } + + return { imageParts, textContent }; +} diff --git a/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts b/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts new file mode 100644 index 0000000000000000000000000000000000000000..17ad14bc102850da618a6954367e8721a70b2359 --- /dev/null +++ b/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts @@ -0,0 +1,212 @@ +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type OpenAI from "openai"; +import type { Stream } from "openai/streaming"; + +/** + * Transform a stream of OpenAI.Chat.ChatCompletion into a stream of TextGenerationStreamOutput + */ +export async function* openAIChatToTextGenerationStream( + completionStream: Stream, + getRouterMetadata?: () => { route?: string; model?: string; provider?: string } +) { + let generatedText = ""; + let tokenId = 0; + let toolBuffer = ""; // legacy hack kept harmless + let metadataYielded = false; + let thinkOpen = false; + + for await (const completion of completionStream) { + const retyped = completion as { + "x-router-metadata"?: { route: string; model: string; provider?: string }; + }; + // Check if this chunk contains router metadata (first chunk from llm-router) + if (!metadataYielded && retyped["x-router-metadata"]) { + const metadata = retyped["x-router-metadata"]; + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: null, + details: null, + routerMetadata: { + route: metadata.route, + model: metadata.model, + provider: metadata.provider, + }, + } as TextGenerationStreamOutput & { + routerMetadata: { route: string; model: string; provider?: string }; + }; + metadataYielded = true; + // Skip processing this chunk as content since it's just metadata + if ( + !completion.choices || + completion.choices.length === 0 || + !completion.choices[0].delta?.content + ) { + continue; + } + } + const { choices } = completion; + const delta: OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta & { + reasoning?: string; + reasoning_content?: string; + } = choices?.[0]?.delta ?? {}; + const content: string = delta.content ?? ""; + const reasoning: string = + typeof delta?.reasoning === "string" + ? (delta.reasoning as string) + : typeof delta?.reasoning_content === "string" + ? (delta.reasoning_content as string) + : ""; + const last = choices?.[0]?.finish_reason === "stop" || choices?.[0]?.finish_reason === "length"; + + // if the last token is a stop and the tool buffer is not empty, yield it as a generated_text + if (choices?.[0]?.finish_reason === "stop" && toolBuffer.length > 0) { + yield { + token: { + id: tokenId++, + special: true, + logprob: 0, + text: "", + }, + generated_text: toolBuffer, + details: null, + } as TextGenerationStreamOutput; + break; + } + + // weird bug where the parameters are streamed in like this + if (choices?.[0]?.delta?.tool_calls) { + const calls = Array.isArray(choices[0].delta.tool_calls) + ? choices[0].delta.tool_calls + : [choices[0].delta.tool_calls]; + + if ( + calls.length === 1 && + calls[0].index === 0 && + calls[0].id === "" && + calls[0].type === "function" && + !!calls[0].function && + calls[0].function.name === null + ) { + toolBuffer += calls[0].function.arguments; + continue; + } + } + + let combined = ""; + if (reasoning && reasoning.length > 0) { + if (!thinkOpen) { + combined += "" + reasoning; + thinkOpen = true; + } else { + combined += reasoning; + } + } + + if (content && content.length > 0) { + const trimmed = content.trim(); + // Allow tags in content to pass through (for models like DeepSeek R1) + if (thinkOpen && trimmed === "") { + // close once without duplicating the tag + combined += ""; + thinkOpen = false; + } else if (thinkOpen) { + combined += "" + content; + thinkOpen = false; + } else { + combined += content; + } + } + + // Accumulate the combined token into the full text + generatedText += combined; + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text: combined, + logprob: 0, + special: last, + }, + generated_text: last ? generatedText : null, + details: null, + }; + yield output; + + // Tools removed: ignore tool_calls deltas + } + + // If metadata wasn't yielded from chunks (e.g., from headers), yield it at the end + if (!metadataYielded && getRouterMetadata) { + const routerMetadata = getRouterMetadata(); + // Yield if we have either complete router metadata OR just provider info + if ( + (routerMetadata && routerMetadata.route && routerMetadata.model) || + routerMetadata?.provider + ) { + yield { + token: { + id: tokenId++, + text: "", + logprob: 0, + special: true, + }, + generated_text: null, + details: null, + routerMetadata, + } as TextGenerationStreamOutput & { + routerMetadata: { route?: string; model?: string; provider?: string }; + }; + } + } +} + +/** + * Transform a non-streaming OpenAI chat completion into a stream of TextGenerationStreamOutput + */ +export async function* openAIChatToTextGenerationSingle( + completion: OpenAI.Chat.Completions.ChatCompletion, + getRouterMetadata?: () => { route?: string; model?: string; provider?: string } +) { + const message: NonNullable["message"] & { + reasoning?: string; + reasoning_content?: string; + } = completion.choices?.[0]?.message ?? {}; + let content: string = message?.content || ""; + // Provider-dependent reasoning shapes (non-streaming) + const r: string = + typeof message?.reasoning === "string" + ? (message.reasoning as string) + : typeof message?.reasoning_content === "string" + ? (message.reasoning_content as string) + : ""; + if (r && r.length > 0) { + content = `${r}` + content; + } + const tokenId = 0; + + // Yield the content as a single token + yield { + token: { + id: tokenId, + text: content, + logprob: 0, + special: false, + }, + generated_text: content, + details: null, + ...(getRouterMetadata + ? (() => { + const metadata = getRouterMetadata(); + return (metadata && metadata.route && metadata.model) || metadata?.provider + ? { routerMetadata: metadata } + : {}; + })() + : {}), + } as TextGenerationStreamOutput & { + routerMetadata?: { route?: string; model?: string; provider?: string }; + }; +} diff --git a/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts b/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c1b30a2aa301b76e0009a3d02ba106073f24c6d --- /dev/null +++ b/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts @@ -0,0 +1,32 @@ +import type { TextGenerationStreamOutput } from "@huggingface/inference"; +import type OpenAI from "openai"; +import type { Stream } from "openai/streaming"; + +/** + * Transform a stream of OpenAI.Completions.Completion into a stream of TextGenerationStreamOutput + */ +export async function* openAICompletionToTextGenerationStream( + completionStream: Stream +) { + let generatedText = ""; + let tokenId = 0; + for await (const completion of completionStream) { + const { choices } = completion; + const text = choices?.[0]?.text ?? ""; + const last = choices?.[0]?.finish_reason === "stop" || choices?.[0]?.finish_reason === "length"; + if (text) { + generatedText = generatedText + text; + } + const output: TextGenerationStreamOutput = { + token: { + id: tokenId++, + text, + logprob: 0, + special: last, + }, + generated_text: last ? generatedText : null, + details: null, + }; + yield output; + } +} diff --git a/src/lib/server/endpoints/preprocessMessages.ts b/src/lib/server/endpoints/preprocessMessages.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8fdb32bf222be050e4f67a8f3de6d28e62ac24b --- /dev/null +++ b/src/lib/server/endpoints/preprocessMessages.ts @@ -0,0 +1,62 @@ +import type { Message } from "$lib/types/Message"; +import type { EndpointMessage } from "./endpoints"; +import { downloadFile } from "../files/downloadFile"; +import type { ObjectId } from "mongodb"; + +export async function preprocessMessages( + messages: Message[], + convId: ObjectId +): Promise { + return Promise.resolve(messages) + .then((msgs) => downloadFiles(msgs, convId)) + .then((msgs) => injectClipboardFiles(msgs)) + .then(stripEmptyInitialSystemMessage); +} + +async function downloadFiles(messages: Message[], convId: ObjectId): Promise { + return Promise.all( + messages.map>((message) => + Promise.all((message.files ?? []).map((file) => downloadFile(file.value, convId))).then( + (files) => ({ ...message, files }) + ) + ) + ); +} + +async function injectClipboardFiles(messages: EndpointMessage[]) { + return Promise.all( + messages.map((message) => { + const plaintextFiles = message.files + ?.filter((file) => file.mime === "application/vnd.chatui.clipboard") + .map((file) => Buffer.from(file.value, "base64").toString("utf-8")); + + if (!plaintextFiles || plaintextFiles.length === 0) return message; + + return { + ...message, + content: `${plaintextFiles.join("\n\n")}\n\n${message.content}`, + files: message.files?.filter((file) => file.mime !== "application/vnd.chatui.clipboard"), + }; + }) + ); +} + +/** + * Remove an initial system message if its content is empty/whitespace only. + * This prevents sending an empty system prompt to any provider. + */ +function stripEmptyInitialSystemMessage(messages: EndpointMessage[]): EndpointMessage[] { + if (!messages?.length) return messages; + const first = messages[0]; + if (first?.from !== "system") return messages; + + const content = first?.content as unknown; + const isEmpty = + typeof content === "string" ? content.trim().length === 0 : false; + + if (isEmpty) { + return messages.slice(1); + } + + return messages; +} diff --git a/src/lib/server/exitHandler.ts b/src/lib/server/exitHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..996815da7fadceaf54a7e702a86b87959ed8ff34 --- /dev/null +++ b/src/lib/server/exitHandler.ts @@ -0,0 +1,59 @@ +import { randomUUID } from "$lib/utils/randomUuid"; +import { timeout } from "$lib/utils/timeout"; +import { logger } from "./logger"; + +type ExitHandler = () => void | Promise; +type ExitHandlerUnsubscribe = () => void; + +const listeners = new Map(); + +export function onExit(cb: ExitHandler): ExitHandlerUnsubscribe { + const uuid = randomUUID(); + listeners.set(uuid, cb); + return () => { + listeners.delete(uuid); + }; +} + +async function runExitHandler(handler: ExitHandler): Promise { + return timeout(Promise.resolve().then(handler), 30_000).catch((err) => { + logger.error(err, "Exit handler failed to run"); + }); +} + +export function initExitHandler() { + let signalCount = 0; + const exitHandler = async () => { + if (signalCount === 1) { + logger.info("Received signal... Exiting"); + await Promise.all(Array.from(listeners.values()).map(runExitHandler)); + logger.info("All exit handlers ran... Waiting for svelte server to exit"); + } + }; + + process.on("SIGINT", () => { + signalCount++; + + if (signalCount >= 2) { + process.kill(process.pid, "SIGKILL"); + } else { + exitHandler().catch((err) => { + logger.error("Exit handler error:", err); + process.kill(process.pid, "SIGKILL"); + }); + } + }); + + process.on("SIGTERM", () => { + signalCount++; + + if (signalCount >= 2) { + process.kill(process.pid, "SIGKILL"); + } else { + exitHandler().catch((err) => { + logger.error("Exit handler error:", err); + process.kill(process.pid, "SIGKILL"); + }); + } + }); +} diff --git a/src/lib/server/files/downloadFile.ts b/src/lib/server/files/downloadFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..d289fc10c85220834b2828ebe4cf32b0776ccc73 --- /dev/null +++ b/src/lib/server/files/downloadFile.ts @@ -0,0 +1,34 @@ +import { error } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import type { Conversation } from "$lib/types/Conversation"; +import type { SharedConversation } from "$lib/types/SharedConversation"; +import type { MessageFile } from "$lib/types/Message"; + +export async function downloadFile( + sha256: string, + convId: Conversation["_id"] | SharedConversation["_id"] +): Promise { + const fileId = collections.bucket.find({ filename: `${convId.toString()}-${sha256}` }); + + const file = await fileId.next(); + if (!file) { + error(404, "File not found"); + } + if (file.metadata?.conversation !== convId.toString()) { + error(403, "You don't have access to this file."); + } + + const mime = file.metadata?.mime; + const name = file.filename; + + const fileStream = collections.bucket.openDownloadStream(file._id); + + const buffer = await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + fileStream.on("data", (chunk) => chunks.push(chunk)); + fileStream.on("error", reject); + fileStream.on("end", () => resolve(Buffer.concat(chunks))); + }); + + return { type: "base64", name, value: buffer.toString("base64"), mime }; +} diff --git a/src/lib/server/files/uploadFile.ts b/src/lib/server/files/uploadFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..97b335beaf00cbad96ce1ef3bbd531cd8ce129ba --- /dev/null +++ b/src/lib/server/files/uploadFile.ts @@ -0,0 +1,29 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { MessageFile } from "$lib/types/Message"; +import { sha256 } from "$lib/utils/sha256"; +import { fileTypeFromBuffer } from "file-type"; +import { collections } from "$lib/server/database"; + +export async function uploadFile(file: File, conv: Conversation): Promise { + const sha = await sha256(await file.text()); + const buffer = await file.arrayBuffer(); + + // Attempt to detect the mime type of the file, fallback to the uploaded mime + const mime = await fileTypeFromBuffer(buffer).then((fileType) => fileType?.mime ?? file.type); + + const upload = collections.bucket.openUploadStream(`${conv._id}-${sha}`, { + metadata: { conversation: conv._id.toString(), mime }, + }); + + upload.write((await file.arrayBuffer()) as unknown as Buffer); + upload.end(); + + // only return the filename when upload throws a finish event or a 20s time out occurs + return new Promise((resolve, reject) => { + upload.once("finish", () => + resolve({ type: "hash", value: sha, mime: file.type, name: file.name }) + ); + upload.once("error", reject); + setTimeout(() => reject(new Error("Upload timed out")), 20_000); + }); +} diff --git a/src/lib/server/findRepoRoot.ts b/src/lib/server/findRepoRoot.ts new file mode 100644 index 0000000000000000000000000000000000000000..e94f397e1da731b110ca38c47b96880b2d573029 --- /dev/null +++ b/src/lib/server/findRepoRoot.ts @@ -0,0 +1,13 @@ +import { existsSync } from "fs"; +import { join, dirname } from "path"; + +export function findRepoRoot(startPath: string): string { + let currentPath = startPath; + while (currentPath !== "/") { + if (existsSync(join(currentPath, "package.json"))) { + return currentPath; + } + currentPath = dirname(currentPath); + } + throw new Error("Could not find repository root (no package.json found)"); +} diff --git a/src/lib/server/generateFromDefaultEndpoint.ts b/src/lib/server/generateFromDefaultEndpoint.ts new file mode 100644 index 0000000000000000000000000000000000000000..e221ab8e58d9c4ea3b2dc096a0894b1552789424 --- /dev/null +++ b/src/lib/server/generateFromDefaultEndpoint.ts @@ -0,0 +1,46 @@ +import { taskModel, models } from "$lib/server/models"; +import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; +import type { EndpointMessage } from "./endpoints/endpoints"; + +export async function* generateFromDefaultEndpoint({ + messages, + preprompt, + generateSettings, + modelId, + locals, +}: { + messages: EndpointMessage[]; + preprompt?: string; + generateSettings?: Record; + /** Optional: use this model instead of the default task model */ + modelId?: string; + locals: App.Locals | undefined; +}): AsyncGenerator { + try { + // Choose endpoint based on provided modelId, else fall back to taskModel + const model = modelId ? (models.find((m) => m.id === modelId) ?? taskModel) : taskModel; + const endpoint = await model.getEndpoint(); + const tokenStream = await endpoint({ messages, preprompt, generateSettings, locals }); + + for await (const output of tokenStream) { + // if not generated_text is here it means the generation is not done + if (output.generated_text) { + let generated_text = output.generated_text; + for (const stop of [...(model.parameters?.stop ?? []), "<|endoftext|>"]) { + if (generated_text.endsWith(stop)) { + generated_text = generated_text.slice(0, -stop.length).trimEnd(); + } + } + return generated_text; + } + yield { + type: MessageUpdateType.Stream, + token: output.token.text, + }; + } + } catch (error) { + return ""; + } + + return ""; +} diff --git a/src/lib/server/isURLLocal.spec.ts b/src/lib/server/isURLLocal.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dda5f4b5aac82244095772a91980d936b8ef431 --- /dev/null +++ b/src/lib/server/isURLLocal.spec.ts @@ -0,0 +1,31 @@ +import { isURLLocal } from "./isURLLocal"; +import { describe, expect, it } from "vitest"; + +describe("isURLLocal", async () => { + it("should return true for localhost", async () => { + expect(await isURLLocal(new URL("http://localhost"))).toBe(true); + }); + it("should return true for 127.0.0.1", async () => { + expect(await isURLLocal(new URL("http://127.0.0.1"))).toBe(true); + }); + it("should return true for 127.254.254.254", async () => { + expect(await isURLLocal(new URL("http://127.254.254.254"))).toBe(true); + }); + it("should return false for huggingface.co", async () => { + expect(await isURLLocal(new URL("https://huggingface.co/"))).toBe(false); + }); + it("should return true for 127.0.0.1.nip.io", async () => { + expect(await isURLLocal(new URL("http://127.0.0.1.nip.io"))).toBe(true); + }); + it("should fail on ipv6", async () => { + await expect(isURLLocal(new URL("http://[::1]"))).rejects.toThrow(); + }); + it("should fail on ipv6 --1.sslip.io", async () => { + await expect(isURLLocal(new URL("http://--1.sslip.io"))).rejects.toThrow(); + }); + it("should fail on invalid domain names", async () => { + await expect( + isURLLocal(new URL("http://34329487239847329874923948732984.com/")) + ).rejects.toThrow(); + }); +}); diff --git a/src/lib/server/isURLLocal.ts b/src/lib/server/isURLLocal.ts new file mode 100644 index 0000000000000000000000000000000000000000..c54043321a995b3d1d4d0dfff4453e1dcdc8a2aa --- /dev/null +++ b/src/lib/server/isURLLocal.ts @@ -0,0 +1,48 @@ +import { Address6, Address4 } from "ip-address"; +import dns from "node:dns"; +import { isIP } from "node:net"; + +const dnsLookup = (hostname: string): Promise<{ address: string; family: number }> => { + return new Promise((resolve, reject) => { + dns.lookup(hostname, (err, address, family) => { + if (err) return reject(err); + resolve({ address, family }); + }); + }); +}; + +export async function isURLLocal(URL: URL): Promise { + const { address, family } = await dnsLookup(URL.hostname); + + if (family === 4) { + const addr = new Address4(address); + const localSubnet = new Address4("127.0.0.0/8"); + return addr.isInSubnet(localSubnet); + } + + if (family === 6) { + const addr = new Address6(address); + return addr.isLoopback() || addr.isInSubnet(new Address6("::1/128")) || addr.isLinkLocal(); + } + + throw Error("Unknown IP family"); +} + +export function isURLStringLocal(url: string) { + try { + const urlObj = new URL(url); + return isURLLocal(urlObj); + } catch (e) { + // assume local if URL parsing fails + return true; + } +} + +export function isHostLocalhost(host: string): boolean { + if (host === "localhost") return true; + if (host === "::1" || host === "[::1]") return true; + if (host.startsWith("127.") && isIP(host)) return true; + if (host.endsWith(".localhost")) return true; + + return false; +} diff --git a/src/lib/server/logger.ts b/src/lib/server/logger.ts new file mode 100644 index 0000000000000000000000000000000000000000..16149e054d3e2245e05e9b058db9d9597dd8f74b --- /dev/null +++ b/src/lib/server/logger.ts @@ -0,0 +1,26 @@ +import pino from "pino"; +import { dev } from "$app/environment"; +import { config } from "$lib/server/config"; + +let options: pino.LoggerOptions = {}; + +if (dev) { + options = { + transport: { + target: "pino-pretty", + options: { + colorize: true, + }, + }, + }; +} + +export const logger = pino({ + ...options, + level: config.LOG_LEVEL || "info", + formatters: { + level: (label) => { + return { level: label }; + }, + }, +}); diff --git a/src/lib/server/metrics.ts b/src/lib/server/metrics.ts new file mode 100644 index 0000000000000000000000000000000000000000..63c152b70521b94938197ae89d710aca06acd101 --- /dev/null +++ b/src/lib/server/metrics.ts @@ -0,0 +1,255 @@ +import { collectDefaultMetrics, Counter, Registry, Summary } from "prom-client"; +import { logger } from "$lib/server/logger"; +import { config } from "$lib/server/config"; +import { createServer, type Server as HttpServer } from "http"; +import { onExit } from "./exitHandler"; + +type ModelLabel = "model"; +type ToolLabel = "tool"; + +interface Metrics { + model: { + conversationsTotal: Counter; + messagesTotal: Counter; + tokenCountTotal: Counter; + timePerOutputToken: Summary; + timeToFirstToken: Summary; + latency: Summary; + votesPositive: Counter; + votesNegative: Counter; + }; + webSearch: { + requestCount: Counter; + pageFetchCount: Counter; + pageFetchCountError: Counter; + pageFetchDuration: Summary; + embeddingDuration: Summary; + }; + tool: { + toolUseCount: Counter; + toolUseCountError: Counter; + toolUseDuration: Summary; + timeToChooseTools: Summary; + }; +} + +export class MetricsServer { + private static instance: MetricsServer | undefined; + private readonly enabled: boolean; + private readonly register: Registry; + private readonly metrics: Metrics; + private httpServer: HttpServer | undefined; + + private constructor() { + this.enabled = config.METRICS_ENABLED === "true"; + this.register = new Registry(); + + if (this.enabled) { + collectDefaultMetrics({ register: this.register }); + } + + this.metrics = this.createMetrics(); + + if (this.enabled) { + this.startStandaloneServer(); + } + } + + public static getInstance(): MetricsServer { + if (!MetricsServer.instance) { + MetricsServer.instance = new MetricsServer(); + } + return MetricsServer.instance; + } + + public static getMetrics(): Metrics { + return MetricsServer.getInstance().metrics; + } + + public static isEnabled(): boolean { + return config.METRICS_ENABLED === "true"; + } + + public async render(): Promise { + if (!this.enabled) { + return ""; + } + + return this.register.metrics(); + } + + private createMetrics(): Metrics { + const labelNames: ModelLabel[] = ["model"]; + const toolLabelNames: ToolLabel[] = ["tool"]; + + const noopRegistry = new Registry(); + + const registry = this.enabled ? this.register : noopRegistry; + + return { + model: { + conversationsTotal: new Counter({ + name: "model_conversations_total", + help: "Total number of conversations", + labelNames, + registers: [registry], + }), + messagesTotal: new Counter({ + name: "model_messages_total", + help: "Total number of messages", + labelNames, + registers: [registry], + }), + tokenCountTotal: new Counter({ + name: "model_token_count_total", + help: "Total number of tokens emitted by the model", + labelNames, + registers: [registry], + }), + timePerOutputToken: new Summary({ + name: "model_time_per_output_token_ms", + help: "Per-token latency in milliseconds", + labelNames, + registers: [registry], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + timeToFirstToken: new Summary({ + name: "model_time_to_first_token_ms", + help: "Time to first token in milliseconds", + labelNames, + registers: [registry], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + latency: new Summary({ + name: "model_latency_ms", + help: "Total time to complete a response in milliseconds", + labelNames, + registers: [registry], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + votesPositive: new Counter({ + name: "model_votes_positive_total", + help: "Total number of positive votes on model messages", + labelNames, + registers: [registry], + }), + votesNegative: new Counter({ + name: "model_votes_negative_total", + help: "Total number of negative votes on model messages", + labelNames, + registers: [registry], + }), + }, + webSearch: { + requestCount: new Counter({ + name: "web_search_request_count", + help: "Total number of web search requests", + registers: [registry], + }), + pageFetchCount: new Counter({ + name: "web_search_page_fetch_count", + help: "Total number of web search page fetches", + registers: [registry], + }), + pageFetchCountError: new Counter({ + name: "web_search_page_fetch_count_error", + help: "Total number of web search page fetch errors", + registers: [registry], + }), + pageFetchDuration: new Summary({ + name: "web_search_page_fetch_duration_ms", + help: "Duration of web search page fetches in milliseconds", + registers: [registry], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + embeddingDuration: new Summary({ + name: "web_search_embedding_duration_ms", + help: "Duration of web search embeddings in milliseconds", + registers: [registry], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + }, + tool: { + toolUseCount: new Counter({ + name: "tool_use_count", + help: "Total number of tool invocations", + labelNames: toolLabelNames, + registers: [registry], + }), + toolUseCountError: new Counter({ + name: "tool_use_count_error", + help: "Total number of tool invocation errors", + labelNames: toolLabelNames, + registers: [registry], + }), + toolUseDuration: new Summary({ + name: "tool_use_duration_ms", + help: "Duration of tool invocations in milliseconds", + labelNames: toolLabelNames, + registers: [registry], + maxAgeSeconds: 30 * 60, + ageBuckets: 5, + }), + timeToChooseTools: new Summary({ + name: "time_to_choose_tools_ms", + help: "Time spent selecting tools in milliseconds", + labelNames, + registers: [registry], + maxAgeSeconds: 5 * 60, + ageBuckets: 5, + }), + }, + }; + } + + private startStandaloneServer() { + const port = Number(config.METRICS_PORT || "5565"); + + if (!Number.isInteger(port) || port < 0 || port > 65535) { + logger.warn(`Invalid METRICS_PORT value: ${config.METRICS_PORT}`); + return; + } + + this.httpServer = createServer(async (req, res) => { + if (req.method !== "GET") { + res.statusCode = 405; + res.end("Method Not Allowed"); + return; + } + + try { + const payload = await this.render(); + res.setHeader("Content-Type", "text/plain; version=0.0.4"); + res.end(payload); + } catch (error) { + logger.error(error, "Failed to render metrics"); + res.statusCode = 500; + res.end("Failed to render metrics"); + } + }); + + this.httpServer.listen(port, () => { + logger.info(`Metrics server listening on port ${port}`); + }); + + onExit(async () => { + if (!this.httpServer) return; + logger.info("Shutting down metrics server..."); + await new Promise((resolve, reject) => { + this.httpServer?.close((err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }).catch((error) => logger.error(error, "Failed to close metrics server")); + this.httpServer = undefined; + }); + } +} diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba1d70ed1af35d57516593e6d56c2232ab81c0d7 --- /dev/null +++ b/src/lib/server/models.ts @@ -0,0 +1,486 @@ +import { config } from "$lib/server/config"; +import type { ChatTemplateInput } from "$lib/types/Template"; +import { z } from "zod"; +import endpoints, { endpointSchema, type Endpoint } from "./endpoints/endpoints"; + +import JSON5 from "json5"; +import { logger } from "$lib/server/logger"; +import { makeRouterEndpoint } from "$lib/server/router/endpoint"; + +type Optional = Pick, K> & Omit; + +const sanitizeJSONEnv = (val: string, fallback: string) => { + const raw = (val ?? "").trim(); + const unquoted = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw; + return unquoted || fallback; +}; + +const modelConfig = z.object({ + /** Used as an identifier in DB */ + id: z.string().optional(), + /** Used to link to the model page, and for inference */ + name: z.string().default(""), + displayName: z.string().min(1).optional(), + description: z.string().min(1).optional(), + logoUrl: z.string().url().optional(), + websiteUrl: z.string().url().optional(), + modelUrl: z.string().url().optional(), + tokenizer: z.never().optional(), + datasetName: z.string().min(1).optional(), + datasetUrl: z.string().url().optional(), + preprompt: z.string().default(""), + prepromptUrl: z.string().url().optional(), + chatPromptTemplate: z.never().optional(), + promptExamples: z + .array( + z.object({ + title: z.string().min(1), + prompt: z.string().min(1), + }) + ) + .optional(), + endpoints: z.array(endpointSchema).optional(), + providers: z.array(z.object({ supports_tools: z.boolean().optional() }).passthrough()).optional(), + parameters: z + .object({ + temperature: z.number().min(0).max(2).optional(), + truncate: z.number().int().positive().optional(), + max_tokens: z.number().int().positive().optional(), + stop: z.array(z.string()).optional(), + top_p: z.number().positive().optional(), + top_k: z.number().positive().optional(), + frequency_penalty: z.number().min(-2).max(2).optional(), + presence_penalty: z.number().min(-2).max(2).optional(), + }) + .passthrough() + .optional(), + multimodal: z.boolean().default(false), + multimodalAcceptedMimetypes: z.array(z.string()).optional(), + unlisted: z.boolean().default(false), + embeddingModel: z.never().optional(), + /** Used to enable/disable system prompt usage */ + systemRoleSupported: z.boolean().default(true), +}); + +type ModelConfig = z.infer; + +const overrideEntrySchema = modelConfig + .partial() + .extend({ + id: z.string().optional(), + name: z.string().optional(), + }) + .refine((value) => Boolean((value.id ?? value.name)?.trim()), { + message: "Model override entry must provide an id or name", + }); + +type ModelOverride = z.infer; + +const openaiBaseUrl = config.OPENAI_BASE_URL + ? config.OPENAI_BASE_URL.replace(/\/$/, "") + : undefined; +const isHFRouter = openaiBaseUrl === "https://router.huggingface.co/v1"; + +const listSchema = z + .object({ + data: z.array( + z.object({ + id: z.string(), + description: z.string().optional(), + providers: z + .array(z.object({ supports_tools: z.boolean().optional() }).passthrough()) + .optional(), + architecture: z + .object({ + input_modalities: z.array(z.string()).optional(), + }) + .passthrough() + .optional(), + }) + ), + }) + .passthrough(); + +function getChatPromptRender(_m: ModelConfig): (inputs: ChatTemplateInput) => string { + // Minimal template to support legacy "completions" flow if ever used. + // We avoid any tokenizer/Jinja usage in this build. + return ({ messages, preprompt }) => { + const parts: string[] = []; + if (preprompt) parts.push(`[SYSTEM]\n${preprompt}`); + for (const msg of messages) { + const role = msg.from === "assistant" ? "ASSISTANT" : msg.from.toUpperCase(); + parts.push(`[${role}]\n${msg.content}`); + } + parts.push(`[ASSISTANT]`); + return parts.join("\n\n"); + }; +} + +const processModel = async (m: ModelConfig) => ({ + ...m, + chatPromptRender: await getChatPromptRender(m), + id: m.id || m.name, + displayName: m.displayName || m.name, + preprompt: m.prepromptUrl ? await fetch(m.prepromptUrl).then((r) => r.text()) : m.preprompt, + parameters: { ...m.parameters, stop_sequences: m.parameters?.stop }, + unlisted: m.unlisted ?? false, +}); + +const addEndpoint = (m: Awaited>) => ({ + ...m, + getEndpoint: async (): Promise => { + if (!m.endpoints || m.endpoints.length === 0) { + throw new Error("No endpoints configured. This build requires OpenAI-compatible endpoints."); + } + // Only support OpenAI-compatible endpoints in this build + const endpoint = m.endpoints[0]; + if (endpoint.type !== "openai") { + throw new Error("Only 'openai' endpoint type is supported in this build"); + } + return await endpoints.openai({ ...endpoint, model: m }); + }, +}); + +type InternalProcessedModel = Awaited> & { + isRouter: boolean; + hasInferenceAPI: boolean; +}; + +const inferenceApiIds: string[] = []; + +const getModelOverrides = (): ModelOverride[] => { + const overridesEnv = (Reflect.get(config, "MODELS") as string | undefined) ?? ""; + + if (!overridesEnv.trim()) { + return []; + } + + try { + return z.array(overrideEntrySchema).parse(JSON5.parse(sanitizeJSONEnv(overridesEnv, "[]"))); + } catch (error) { + logger.error(error, "[models] Failed to parse MODELS overrides"); + return []; + } +}; + +export type ModelsRefreshSummary = { + refreshedAt: Date; + durationMs: number; + added: string[]; + removed: string[]; + changed: string[]; + total: number; +}; + +export type ProcessedModel = InternalProcessedModel; + +export let models: ProcessedModel[] = []; +export let defaultModel!: ProcessedModel; +export let taskModel!: ProcessedModel; +export let validModelIdSchema: z.ZodType = z.string(); +export let lastModelRefresh = new Date(0); +export let lastModelRefreshDurationMs = 0; +export let lastModelRefreshSummary: ModelsRefreshSummary = { + refreshedAt: new Date(0), + durationMs: 0, + added: [], + removed: [], + changed: [], + total: 0, +}; + +let inflightRefresh: Promise | null = null; + +const createValidModelIdSchema = (modelList: ProcessedModel[]): z.ZodType => { + if (modelList.length === 0) { + throw new Error("No models available to build validation schema"); + } + const ids = new Set(modelList.map((m) => m.id)); + return z.string().refine((value) => ids.has(value), "Invalid model id"); +}; + +const resolveTaskModel = (modelList: ProcessedModel[]) => { + if (modelList.length === 0) { + throw new Error("No models available to select task model"); + } + + if (config.TASK_MODEL) { + const preferred = modelList.find( + (m) => m.name === config.TASK_MODEL || m.id === config.TASK_MODEL + ); + if (preferred) { + return preferred; + } + } + + return modelList[0]; +}; + +const signatureForModel = (model: ProcessedModel) => + JSON.stringify({ + description: model.description, + displayName: model.displayName, + providers: model.providers, + parameters: model.parameters, + preprompt: model.preprompt, + prepromptUrl: model.prepromptUrl, + endpoints: + model.endpoints?.map((endpoint) => { + if (endpoint.type === "openai") { + const { type, baseURL } = endpoint; + return { type, baseURL }; + } + return { type: endpoint.type }; + }) ?? null, + multimodal: model.multimodal, + multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes, + isRouter: model.isRouter, + hasInferenceAPI: model.hasInferenceAPI, + }); + +const applyModelState = (newModels: ProcessedModel[], startedAt: number): ModelsRefreshSummary => { + if (newModels.length === 0) { + throw new Error("Failed to load any models from upstream"); + } + + const previousIds = new Set(models.map((m) => m.id)); + const previousSignatures = new Map(models.map((m) => [m.id, signatureForModel(m)])); + const refreshedAt = new Date(); + const durationMs = Date.now() - startedAt; + + models = newModels; + defaultModel = models[0]; + taskModel = resolveTaskModel(models); + validModelIdSchema = createValidModelIdSchema(models); + lastModelRefresh = refreshedAt; + lastModelRefreshDurationMs = durationMs; + + const added = newModels.map((m) => m.id).filter((id) => !previousIds.has(id)); + const removed = Array.from(previousIds).filter( + (id) => !newModels.some((model) => model.id === id) + ); + const changed = newModels + .filter((model) => { + const previousSignature = previousSignatures.get(model.id); + return previousSignature !== undefined && previousSignature !== signatureForModel(model); + }) + .map((model) => model.id); + + const summary: ModelsRefreshSummary = { + refreshedAt, + durationMs, + added, + removed, + changed, + total: models.length, + }; + + lastModelRefreshSummary = summary; + + logger.info( + { + total: summary.total, + added: summary.added, + removed: summary.removed, + changed: summary.changed, + durationMs: summary.durationMs, + }, + "[models] Model cache refreshed" + ); + + return summary; +}; + +const buildModels = async (): Promise => { + if (!openaiBaseUrl) { + logger.error( + "OPENAI_BASE_URL is required. Set it to an OpenAI-compatible base (e.g., https://router.huggingface.co/v1)." + ); + throw new Error("OPENAI_BASE_URL not set"); + } + + try { + const baseURL = openaiBaseUrl; + logger.info({ baseURL }, "[models] Using OpenAI-compatible base URL"); + + // Canonical auth token is OPENAI_API_KEY; keep HF_TOKEN as legacy alias + const authToken = config.OPENAI_API_KEY || config.HF_TOKEN; + + // Use auth token from the start if available to avoid rate limiting issues + // Some APIs rate-limit unauthenticated requests more aggressively + const response = await fetch(`${baseURL}/models`, { + headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined, + }); + logger.info({ status: response.status }, "[models] First fetch status"); + if (!response.ok && response.status === 401 && !authToken) { + // If we get 401 and didn't have a token, there's nothing we can do + throw new Error( + `Failed to fetch ${baseURL}/models: ${response.status} ${response.statusText} (no auth token available)` + ); + } + if (!response.ok) { + throw new Error( + `Failed to fetch ${baseURL}/models: ${response.status} ${response.statusText}` + ); + } + const json = await response.json(); + logger.info({ keys: Object.keys(json || {}) }, "[models] Response keys"); + + const parsed = listSchema.parse(json); + logger.info({ count: parsed.data.length }, "[models] Parsed models count"); + + let modelsRaw = parsed.data.map((m) => { + let logoUrl: string | undefined = undefined; + if (isHFRouter && m.id.includes("/")) { + const org = m.id.split("/")[0]; + logoUrl = `https://huggingface.co/api/organizations/${encodeURIComponent(org)}/avatar?redirect=true`; + } + + const inputModalities = (m.architecture?.input_modalities ?? []).map((modality) => + modality.toLowerCase() + ); + const supportsImageInput = + inputModalities.includes("image") || inputModalities.includes("vision"); + return { + id: m.id, + name: m.id, + displayName: m.id, + description: m.description, + logoUrl, + providers: m.providers, + multimodal: supportsImageInput, + multimodalAcceptedMimetypes: supportsImageInput ? ["image/*"] : undefined, + endpoints: [ + { + type: "openai" as const, + baseURL, + // apiKey will be taken from OPENAI_API_KEY or HF_TOKEN automatically + }, + ], + } as ModelConfig; + }) as ModelConfig[]; + + const overrides = getModelOverrides(); + + if (overrides.length) { + const overrideMap = new Map(); + for (const override of overrides) { + for (const key of [override.id, override.name]) { + const trimmed = key?.trim(); + if (trimmed) overrideMap.set(trimmed, override); + } + } + + modelsRaw = modelsRaw.map((model) => { + const override = overrideMap.get(model.id ?? "") ?? overrideMap.get(model.name ?? ""); + if (!override) return model; + + const { id, name, ...rest } = override; + void id; + void name; + + return { + ...model, + ...rest, + }; + }); + } + + const builtModels = await Promise.all( + modelsRaw.map((e) => + processModel(e) + .then(addEndpoint) + .then(async (m) => ({ + ...m, + hasInferenceAPI: inferenceApiIds.includes(m.id ?? m.name), + // router decoration added later + isRouter: false as boolean, + })) + ) + ); + + const archBase = (config.LLM_ROUTER_ARCH_BASE_URL || "").trim(); + const routerLabel = (config.PUBLIC_LLM_ROUTER_DISPLAY_NAME || "Omni").trim() || "Omni"; + const routerLogo = (config.PUBLIC_LLM_ROUTER_LOGO_URL || "").trim(); + const routerAliasId = (config.PUBLIC_LLM_ROUTER_ALIAS_ID || "omni").trim() || "omni"; + const routerMultimodalEnabled = + (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true"; + + let decorated = builtModels as ProcessedModel[]; + + if (archBase) { + // Build a minimal model config for the alias + const aliasRaw = { + id: routerAliasId, + name: routerAliasId, + displayName: routerLabel, + description: "Automatically routes your messages to the best model for your request.", + logoUrl: routerLogo || undefined, + preprompt: "", + endpoints: [ + { + type: "openai" as const, + baseURL: openaiBaseUrl, + }, + ], + // Keep the alias visible + unlisted: false, + } as ModelConfig; + + if (routerMultimodalEnabled) { + aliasRaw.multimodal = true; + aliasRaw.multimodalAcceptedMimetypes = ["image/*"]; + } + + const aliasBase = await processModel(aliasRaw); + // Create a self-referential ProcessedModel for the router endpoint + const aliasModel: ProcessedModel = { + ...aliasBase, + isRouter: true, + hasInferenceAPI: false, + // getEndpoint uses the router wrapper regardless of the endpoints array + getEndpoint: async (): Promise => makeRouterEndpoint(aliasModel), + } as ProcessedModel; + + // Put alias first + decorated = [aliasModel, ...decorated]; + } + + return decorated; + } catch (e) { + logger.error(e, "Failed to load models from OpenAI base URL"); + throw e; + } +}; + +const rebuildModels = async (): Promise => { + const startedAt = Date.now(); + const newModels = await buildModels(); + return applyModelState(newModels, startedAt); +}; + +await rebuildModels(); + +export const refreshModels = async (): Promise => { + if (inflightRefresh) { + return inflightRefresh; + } + + inflightRefresh = rebuildModels().finally(() => { + inflightRefresh = null; + }); + + return inflightRefresh; +}; + +export const validateModel = (_models: BackendModel[]) => { + // Zod enum function requires 2 parameters + return z.enum([_models[0].id, ..._models.slice(1).map((m) => m.id)]); +}; + +// if `TASK_MODEL` is string & name of a model in `MODELS`, then we use `MODELS[TASK_MODEL]`, else we try to parse `TASK_MODEL` as a model config itself + +export type BackendModel = Optional< + typeof defaultModel, + "preprompt" | "parameters" | "multimodal" | "unlisted" | "hasInferenceAPI" +>; diff --git a/src/lib/server/router/arch.ts b/src/lib/server/router/arch.ts new file mode 100644 index 0000000000000000000000000000000000000000..539f88a32a44e4868fd3d1261800ba7e8bc88aed --- /dev/null +++ b/src/lib/server/router/arch.ts @@ -0,0 +1,226 @@ +import { config } from "$lib/server/config"; +import { logger } from "$lib/server/logger"; +import type { EndpointMessage } from "../endpoints/endpoints"; +import type { Route, RouteConfig, RouteSelection } from "./types"; +import { getRoutes } from "./policy"; +import { getApiToken } from "$lib/server/apiToken"; + +const DEFAULT_LAST_TURNS = 16; + +/** + * Trim a message by keeping start and end, replacing middle with minimal indicator. + * Uses simple ellipsis since router only needs context for intent classification, not exact content. + * @param content - The message content to trim + * @param maxLength - Maximum total length (including indicator) + * @returns Trimmed content with start, ellipsis, and end + */ +function trimMiddle(content: string, maxLength: number): string { + if (content.length <= maxLength) return content; + + const indicator = "…"; + const availableLength = maxLength - indicator.length; + + if (availableLength <= 0) { + // If no room even for indicator, just hard truncate + return content.slice(0, maxLength); + } + + // Reserve more space for the start (typically contains context) + const startLength = Math.ceil(availableLength * 0.6); + const endLength = availableLength - startLength; + + // Bug fix: slice(-0) returns entire string, so check for endLength <= 0 + if (endLength <= 0) { + // Not enough space for end portion, just use start + indicator + return content.slice(0, availableLength) + indicator; + } + + const start = content.slice(0, startLength); + const end = content.slice(-endLength); + + return start + indicator + end; +} + +const PROMPT_TEMPLATE = ` +You are a helpful assistant designed to find the best suited route. +You are provided with route description within XML tags: + + + +{routes} + + + + + +{conversation} + + + +Your task is to decide which route is best suit with user intent on the conversation in XML tags. + +Follow those instructions: +1. Use prior turns to choose the best route for the current message if needed. +2. If no route match the full conversation respond with other route {"route": "other"}. +3. Analyze the route descriptions and find the best match route for user latest intent. +4. Respond only with the route name that best matches the user's request, using the exact name in the block. +Based on your analysis, provide your response in the following JSON format if you decide to match any route: +{"route": "route_name"} +`.trim(); + +function lastNTurns(arr: T[], n = DEFAULT_LAST_TURNS) { + if (!Array.isArray(arr)) return [] as T[]; + return arr.slice(-n); +} + +function toRouterPrompt(messages: EndpointMessage[], routes: Route[]) { + const simpleRoutes: RouteConfig[] = routes.map((r) => ({ + name: r.name, + description: r.description, + })); + const maxAssistantLength = parseInt(config.LLM_ROUTER_MAX_ASSISTANT_LENGTH || "1000", 10); + const maxPrevUserLength = parseInt(config.LLM_ROUTER_MAX_PREV_USER_LENGTH || "1000", 10); + + const convo = messages + .map((m) => ({ role: m.from, content: m.content })) + .filter((m) => typeof m.content === "string" && m.content.trim() !== ""); + + // Find the last user message index to preserve its full content + const lastUserIndex = convo.findLastIndex((m) => m.role === "user"); + + const trimmedConvo = convo.map((m, idx) => { + if (typeof m.content !== "string") return m; + + // Trim assistant messages to reduce routing prompt size and improve latency + // Keep start and end for better context understanding + if (m.role === "assistant") { + return { + ...m, + content: trimMiddle(m.content, maxAssistantLength), + }; + } + + // Trim previous user messages, but keep the latest user message full + // Keep start and end to preserve both context and question + if (m.role === "user" && idx !== lastUserIndex) { + return { + ...m, + content: trimMiddle(m.content, maxPrevUserLength), + }; + } + + return m; + }); + + return PROMPT_TEMPLATE.replace("{routes}", JSON.stringify(simpleRoutes)).replace( + "{conversation}", + JSON.stringify(lastNTurns(trimmedConvo)) + ); +} + +function parseRouteName(text: string): string | undefined { + if (!text) return; + try { + const obj = JSON.parse(text); + if (typeof obj?.route === "string" && obj.route.trim()) return obj.route.trim(); + } catch {} + const m = text.match(/["']route["']\s*:\s*["']([^"']+)["']/); + if (m?.[1]) return m[1].trim(); + try { + const obj = JSON.parse(text.replace(/'/g, '"')); + if (typeof obj?.route === "string" && obj.route.trim()) return obj.route.trim(); + } catch {} + return; +} + +export async function archSelectRoute( + messages: EndpointMessage[], + traceId: string | undefined, + locals: App.Locals | undefined +): Promise { + const routes = await getRoutes(); + const prompt = toRouterPrompt(messages, routes); + + const baseURL = (config.LLM_ROUTER_ARCH_BASE_URL || "").replace(/\/$/, ""); + const archModel = config.LLM_ROUTER_ARCH_MODEL || "router/omni"; + + if (!baseURL) { + logger.warn("LLM_ROUTER_ARCH_BASE_URL not set; routing will fail over to fallback."); + return { routeName: "arch_router_failure" }; + } + + const headers: HeadersInit = { + Authorization: `Bearer ${getApiToken(locals)}`, + "Content-Type": "application/json", + }; + const body = { + model: archModel, + messages: [{ role: "user", content: prompt }], + temperature: 0, + max_tokens: 16, + stream: false, + }; + + const ctrl = new AbortController(); + const timeoutMs = Number(config.LLM_ROUTER_ARCH_TIMEOUT_MS || 10000); + const to = setTimeout(() => ctrl.abort(), timeoutMs); + + try { + const resp = await fetch(`${baseURL}/chat/completions`, { + method: "POST", + headers, + body: JSON.stringify(body), + signal: ctrl.signal, + }); + clearTimeout(to); + if (!resp.ok) { + // Extract error message from response + let errorMessage = `arch-router ${resp.status}`; + try { + const errorData = await resp.json(); + // Try to extract message from OpenAI-style error format + if (errorData.error?.message) { + errorMessage = errorData.error.message; + } else if (errorData.message) { + errorMessage = errorData.message; + } + } catch { + // If JSON parsing fails, use status text + errorMessage = resp.statusText || errorMessage; + } + + logger.warn( + { status: resp.status, error: errorMessage, traceId }, + "[arch] router returned error" + ); + + return { + routeName: "arch_router_failure", + error: { + message: errorMessage, + statusCode: resp.status, + }, + }; + } + const data: { choices: { message: { content: string } }[] } = await resp.json(); + const text = (data?.choices?.[0]?.message?.content ?? "").toString().trim(); + const raw = parseRouteName(text); + + const other = config.LLM_ROUTER_OTHER_ROUTE || "casual_conversation"; + const chosen = raw === "other" ? other : raw || "casual_conversation"; + const exists = routes.some((r) => r.name === chosen); + return { routeName: exists ? chosen : "casual_conversation" }; + } catch (e) { + clearTimeout(to); + const err = e as Error; + logger.warn({ err: String(e), traceId }, "arch router selection failed"); + + // Return error with context but no status code (network/timeout errors) + return { + routeName: "arch_router_failure", + error: { + message: err.message || String(e), + }, + }; + } +} diff --git a/src/lib/server/router/endpoint.ts b/src/lib/server/router/endpoint.ts new file mode 100644 index 0000000000000000000000000000000000000000..54ef59a8471a0adc1aa92ecadd10edb628421c2d --- /dev/null +++ b/src/lib/server/router/endpoint.ts @@ -0,0 +1,290 @@ +import type { + Endpoint, + EndpointParameters, + EndpointMessage, + TextGenerationStreamOutputSimplified, +} from "../endpoints/endpoints"; +import endpoints from "../endpoints/endpoints"; +import type { ProcessedModel } from "../models"; +import { config } from "$lib/server/config"; +import { logger } from "$lib/server/logger"; +import { archSelectRoute } from "./arch"; +import { getRoutes, resolveRouteModels } from "./policy"; +import { getApiToken } from "$lib/server/apiToken"; +import { ROUTER_FAILURE } from "./types"; + +const REASONING_BLOCK_REGEX = /[\s\S]*?(?:<\/think>|$)/g; + +const ROUTER_MULTIMODAL_ROUTE = "multimodal"; + +// Cache models at module level to avoid redundant dynamic imports on every request +let cachedModels: ProcessedModel[] | undefined; + +async function getModels(): Promise { + if (!cachedModels) { + const mod = await import("../models"); + cachedModels = (mod as { models: ProcessedModel[] }).models; + } + return cachedModels; +} + +/** + * Custom error class that preserves HTTP status codes + */ +class HTTPError extends Error { + constructor( + message: string, + public statusCode?: number + ) { + super(message); + this.name = "HTTPError"; + } +} + +/** + * Extract the actual error message and status from OpenAI SDK errors or other upstream errors + */ +function extractUpstreamError(error: unknown): { message: string; statusCode?: number } { + // Check if it's an OpenAI APIError with structured error info + if (error && typeof error === "object") { + const err = error as Record; + + // OpenAI SDK error with error.error.message and status + if ( + err.error && + typeof err.error === "object" && + "message" in err.error && + typeof err.error.message === "string" + ) { + return { + message: err.error.message, + statusCode: typeof err.status === "number" ? err.status : undefined, + }; + } + + // HTTPError or error with statusCode + if (typeof err.statusCode === "number" && typeof err.message === "string") { + return { message: err.message, statusCode: err.statusCode }; + } + + // Error with status field + if (typeof err.status === "number" && typeof err.message === "string") { + return { message: err.message, statusCode: err.status }; + } + + // Direct error message + if (typeof err.message === "string") { + return { message: err.message }; + } + } + + return { message: String(error) }; +} + +/** + * Determines if an error is a policy/entitlement error that should be shown to users immediately + * (vs transient errors that should trigger fallback) + */ +function isPolicyError(statusCode?: number): boolean { + if (!statusCode) return false; + // 400: Bad Request, 402: Payment Required, 401: Unauthorized, 403: Forbidden + return statusCode === 400 || statusCode === 401 || statusCode === 402 || statusCode === 403; +} + +function stripReasoningBlocks(text: string): string { + const stripped = text.replace(REASONING_BLOCK_REGEX, ""); + return stripped === text ? text : stripped.trim(); +} + +function stripReasoningFromMessage(message: EndpointMessage): EndpointMessage { + const content = + typeof message.content === "string" ? stripReasoningBlocks(message.content) : message.content; + return { + ...message, + content, + }; +} + +/** + * Create an Endpoint that performs route selection via Arch and then forwards + * to the selected model (with fallbacks) using the OpenAI-compatible endpoint. + */ +export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise { + return async function routerEndpoint(params: EndpointParameters) { + const routes = await getRoutes(); + const sanitizedMessages = params.messages.map(stripReasoningFromMessage); + const routerMultimodalEnabled = + (config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true"; + const hasImageInput = sanitizedMessages.some((message) => + (message.files ?? []).some( + (file) => typeof file?.mime === "string" && file.mime.startsWith("image/") + ) + ); + + // Helper to create an OpenAI endpoint for a specific candidate model id + async function createCandidateEndpoint(candidateModelId: string): Promise { + // Try to use the real candidate model config if present in chat-ui's model list + let modelForCall: ProcessedModel | undefined; + try { + const all = await getModels(); + modelForCall = all?.find((m) => m.id === candidateModelId || m.name === candidateModelId); + } catch (e) { + logger.warn({ err: String(e) }, "[router] failed to load models for candidate lookup"); + } + + if (!modelForCall) { + // Fallback: clone router model with candidate id + modelForCall = { + ...routerModel, + id: candidateModelId, + name: candidateModelId, + displayName: candidateModelId, + } as ProcessedModel; + } + + return endpoints.openai({ + type: "openai", + baseURL: (config.OPENAI_BASE_URL || "https://router.huggingface.co/v1").replace(/\/$/, ""), + apiKey: getApiToken(params.locals), + model: modelForCall, + // Ensure streaming path is used + streamingSupported: true, + }); + } + + // Yield router metadata for immediate UI display, using the actual candidate + async function* metadataThenStream( + gen: AsyncGenerator, + actualModel: string, + selectedRoute: string + ) { + yield { + token: { id: 0, text: "", special: true, logprob: 0 }, + generated_text: null, + details: null, + routerMetadata: { route: selectedRoute, model: actualModel }, + }; + for await (const ev of gen) yield ev; + } + + async function findFirstMultimodalCandidateId(): Promise { + try { + const all = await getModels(); + + // Check if a specific multimodal model is configured via env variable + const preferredModelId = config.LLM_ROUTER_MULTIMODAL_MODEL; + if (preferredModelId) { + const preferredModel = all?.find( + (m) => (m.id === preferredModelId || m.name === preferredModelId) && m.multimodal + ); + if (preferredModel) { + logger.info( + { model: preferredModel.id ?? preferredModel.name }, + "[router] using configured multimodal model" + ); + return preferredModel.id ?? preferredModel.name; + } + logger.warn( + { configuredModel: preferredModelId }, + "[router] configured multimodal model not found or not multimodal, falling back to first available" + ); + } + + // Fallback to first multimodal model + const first = all?.find((m) => !m.isRouter && m.multimodal); + return first?.id ?? first?.name; + } catch (e) { + logger.warn({ err: String(e) }, "[router] failed to load models for multimodal lookup"); + return undefined; + } + } + + if (routerMultimodalEnabled && hasImageInput) { + const multimodalCandidate = await findFirstMultimodalCandidateId(); + if (!multimodalCandidate) { + throw new Error( + "No multimodal models are configured for the router. Remove the image or enable a multimodal model." + ); + } + + try { + logger.info( + { route: ROUTER_MULTIMODAL_ROUTE, model: multimodalCandidate }, + "[router] multimodal input detected; bypassing Arch selection" + ); + const ep = await createCandidateEndpoint(multimodalCandidate); + const gen = await ep({ ...params }); + return metadataThenStream(gen, multimodalCandidate, ROUTER_MULTIMODAL_ROUTE); + } catch (e) { + const { message, statusCode } = extractUpstreamError(e); + logger.error( + { + route: ROUTER_MULTIMODAL_ROUTE, + model: multimodalCandidate, + err: message, + ...(statusCode && { status: statusCode }), + }, + "[router] multimodal fallback failed" + ); + throw statusCode ? new HTTPError(message, statusCode) : new Error(message); + } + } + + const routeSelection = await archSelectRoute(sanitizedMessages, undefined, params.locals); + + // If arch router failed with an error, only hard-fail for policy errors (402/401/403) + // For transient errors (5xx, timeouts, network), allow fallback to continue + if (routeSelection.routeName === ROUTER_FAILURE && routeSelection.error) { + const { message, statusCode } = routeSelection.error; + + if (isPolicyError(statusCode)) { + // Policy errors should be surfaced to the user immediately (e.g., subscription required) + logger.error( + { err: message, ...(statusCode && { status: statusCode }) }, + "[router] arch router failed with policy error, propagating to client" + ); + throw statusCode ? new HTTPError(message, statusCode) : new Error(message); + } + + // Transient errors: log and continue to fallback + logger.warn( + { err: message, ...(statusCode && { status: statusCode }) }, + "[router] arch router failed with transient error, attempting fallback" + ); + } + + const fallbackModel = config.LLM_ROUTER_FALLBACK_MODEL || routerModel.id; + const { candidates } = resolveRouteModels(routeSelection.routeName, routes, fallbackModel); + + let lastErr: unknown = undefined; + for (const candidate of candidates) { + try { + logger.info( + { route: routeSelection.routeName, model: candidate }, + "[router] trying candidate" + ); + const ep = await createCandidateEndpoint(candidate); + const gen = await ep({ ...params }); + return metadataThenStream(gen, candidate, routeSelection.routeName); + } catch (e) { + lastErr = e; + const { message: errMsg, statusCode: errStatus } = extractUpstreamError(e); + logger.warn( + { + route: routeSelection.routeName, + model: candidate, + err: errMsg, + ...(errStatus && { status: errStatus }), + }, + "[router] candidate failed" + ); + continue; + } + } + + // Exhausted all candidates — throw to signal upstream failure + // Forward the upstream error to the client + const { message, statusCode } = extractUpstreamError(lastErr); + throw statusCode ? new HTTPError(message, statusCode) : new Error(message); + }; +} diff --git a/src/lib/server/router/policy.ts b/src/lib/server/router/policy.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d625a28cf98cdcbc27d30881c277c740b36b71a --- /dev/null +++ b/src/lib/server/router/policy.ts @@ -0,0 +1,49 @@ +import { readFile } from "node:fs/promises"; +import { config } from "$lib/server/config"; +import type { Route } from "./types"; + +let ROUTES: Route[] = []; +let loaded = false; + +export async function loadPolicy(): Promise { + const path = config.LLM_ROUTER_ROUTES_PATH; + const text = await readFile(path, "utf8"); + const arr = JSON.parse(text) as Route[]; + if (!Array.isArray(arr)) { + throw new Error("Routes config must be a flat array of routes"); + } + const seen = new Set(); + for (const r of arr) { + if (!r?.name || !r?.description || !r?.primary_model) { + throw new Error(`Invalid route entry: ${JSON.stringify(r)}`); + } + if (seen.has(r.name)) { + throw new Error(`Duplicate route name: ${r.name}`); + } + seen.add(r.name); + } + ROUTES = arr; + loaded = true; + return ROUTES; +} + +export async function getRoutes(): Promise { + if (!loaded) await loadPolicy(); + return ROUTES; +} + +export function resolveRouteModels( + routeName: string, + routes: Route[], + fallbackModel: string +): { candidates: string[] } { + if (routeName === "arch_router_failure") { + return { candidates: [fallbackModel] }; + } + const sel = + routes.find((r) => r.name === routeName) || + routes.find((r) => r.name === "casual_conversation"); + if (!sel) return { candidates: [fallbackModel] }; + const fallbacks = Array.isArray(sel.fallback_models) ? sel.fallback_models : []; + return { candidates: [sel.primary_model, ...fallbacks] }; +} diff --git a/src/lib/server/router/types.ts b/src/lib/server/router/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce3ea5140d024e320771c4a8232fa0da7e2e9def --- /dev/null +++ b/src/lib/server/router/types.ts @@ -0,0 +1,21 @@ +export interface Route { + name: string; + description: string; + primary_model: string; + fallback_models?: string[]; +} + +export interface RouteConfig { + name: string; + description: string; +} + +export interface RouteSelection { + routeName: string; + error?: { + message: string; + statusCode?: number; + }; +} + +export const ROUTER_FAILURE = "arch_router_failure"; diff --git a/src/lib/server/sendSlack.ts b/src/lib/server/sendSlack.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd892b34b9a25c4313ac94f5d5a12f0f8d1f9c08 --- /dev/null +++ b/src/lib/server/sendSlack.ts @@ -0,0 +1,23 @@ +import { config } from "$lib/server/config"; +import { logger } from "$lib/server/logger"; + +export async function sendSlack(text: string) { + if (!config.WEBHOOK_URL_REPORT_ASSISTANT) { + logger.warn("WEBHOOK_URL_REPORT_ASSISTANT is not set, tried to send a slack message."); + return; + } + + const res = await fetch(config.WEBHOOK_URL_REPORT_ASSISTANT, { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify({ + text, + }), + }); + + if (!res.ok) { + logger.error(`Webhook message failed. ${res.statusText} ${res.text}`); + } +} diff --git a/src/lib/server/textGeneration/generate.ts b/src/lib/server/textGeneration/generate.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae9c608c7c22e0c066bad695e09e3280c8b64752 --- /dev/null +++ b/src/lib/server/textGeneration/generate.ts @@ -0,0 +1,91 @@ +import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; +import { AbortedGenerations } from "../abortedGenerations"; +import type { TextGenerationContext } from "./types"; +import type { EndpointMessage } from "../endpoints/endpoints"; +import { logger } from "../logger"; + +type GenerateContext = Omit & { messages: EndpointMessage[] }; + +export async function* generate( + { + model, + endpoint, + conv, + messages, + assistant, + promptedAt, + forceMultimodal, + locals, + abortController, + }: GenerateContext, + preprompt?: string +): AsyncIterable { + const stream = await endpoint({ + messages, + preprompt, + generateSettings: assistant?.generateSettings, + // Allow user-level override to force multimodal + isMultimodal: (forceMultimodal ?? false) || model.multimodal, + conversationId: conv._id, + locals, + abortSignal: abortController.signal, + }); + + for await (const output of stream) { + // Check if this output contains router metadata + if ( + "routerMetadata" in output && + output.routerMetadata && + ((output.routerMetadata.route && output.routerMetadata.model) || + output.routerMetadata.provider) + ) { + yield { + type: MessageUpdateType.RouterMetadata, + route: output.routerMetadata.route || "", + model: output.routerMetadata.model || "", + provider: output.routerMetadata.provider, + }; + continue; + } + // text generation completed + if (output.generated_text) { + let interrupted = + !output.token.special && !model.parameters.stop?.includes(output.token.text); + + let text = output.generated_text.trimEnd(); + for (const stopToken of model.parameters.stop ?? []) { + if (!text.endsWith(stopToken)) continue; + + interrupted = false; + text = text.slice(0, text.length - stopToken.length); + } + + yield { + type: MessageUpdateType.FinalAnswer, + text, + interrupted, + }; + continue; + } + + // ignore special tokens + if (output.token.special) continue; + + // yield normal token + yield { type: MessageUpdateType.Stream, token: output.token.text }; + + // abort check + const date = AbortedGenerations.getInstance().getAbortTime(conv._id.toString()); + + if (date && date > promptedAt) { + logger.info(`Aborting generation for conversation ${conv._id}`); + if (!abortController.signal.aborted) { + abortController.abort(); + } + break; + } + + // no output check + if (!output) break; + } +} diff --git a/src/lib/server/textGeneration/index.ts b/src/lib/server/textGeneration/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7b7c70a1c7a86bc98a41b511cbfd7bc272ab8b8 --- /dev/null +++ b/src/lib/server/textGeneration/index.ts @@ -0,0 +1,52 @@ +import { preprocessMessages } from "../endpoints/preprocessMessages"; + +import { generateTitleForConversation } from "./title"; +import { + type MessageUpdate, + MessageUpdateType, + MessageUpdateStatus, +} from "$lib/types/MessageUpdate"; +import { generate } from "./generate"; +import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators"; +import type { TextGenerationContext } from "./types"; + +async function* keepAlive(done: AbortSignal): AsyncGenerator { + while (!done.aborted) { + yield { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.KeepAlive, + }; + await new Promise((resolve) => setTimeout(resolve, 100)); + } +} + +export async function* textGeneration(ctx: TextGenerationContext) { + const done = new AbortController(); + + const titleGen = generateTitleForConversation(ctx.conv, ctx.locals); + const textGen = textGenerationWithoutTitle(ctx, done); + const keepAliveGen = keepAlive(done.signal); + + // keep alive until textGen is done + + yield* mergeAsyncGenerators([titleGen, textGen, keepAliveGen]); +} + +async function* textGenerationWithoutTitle( + ctx: TextGenerationContext, + done: AbortController +): AsyncGenerator { + yield { + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Started, + }; + + const { conv, messages } = ctx; + const convId = conv._id; + + const preprompt = conv.preprompt; + + const processedMessages = await preprocessMessages(messages, convId); + yield* generate({ ...ctx, messages: processedMessages }, preprompt); + done.abort(); +} diff --git a/src/lib/server/textGeneration/title.ts b/src/lib/server/textGeneration/title.ts new file mode 100644 index 0000000000000000000000000000000000000000..66e5bd8eff9ad9ef3000f1d1683b352a8c4194e8 --- /dev/null +++ b/src/lib/server/textGeneration/title.ts @@ -0,0 +1,83 @@ +import { config } from "$lib/server/config"; +import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint"; +import { logger } from "$lib/server/logger"; +import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; +import type { Conversation } from "$lib/types/Conversation"; +import { getReturnFromGenerator } from "$lib/utils/getReturnFromGenerator"; + +export async function* generateTitleForConversation( + conv: Conversation, + locals: App.Locals | undefined +): AsyncGenerator { + try { + const userMessage = conv.messages.find((m) => m.from === "user"); + // HACK: detect if the conversation is new + if (conv.title !== "New Chat" || !userMessage) return; + + const prompt = userMessage.content; + const modelForTitle = config.TASK_MODEL?.trim() ? config.TASK_MODEL : conv.model; + const title = (await generateTitle(prompt, modelForTitle, locals)) ?? "New Chat"; + + yield { + type: MessageUpdateType.Title, + title, + }; + } catch (cause) { + logger.error(Error("Failed whilte generating title for conversation", { cause })); + } +} + +async function generateTitle( + prompt: string, + modelId: string | undefined, + locals: App.Locals | undefined +) { + if (config.LLM_SUMMARIZATION !== "true") { + // When summarization is disabled, use the first five words without adding emojis + return prompt.split(/\s+/g).slice(0, 5).join(" "); + } + + // Tools removed: no tool-based title path + + return await getReturnFromGenerator( + generateFromDefaultEndpoint({ + messages: [{ from: "user", content: `User message: "${prompt}"` }], + preprompt: `You are a chat thread titling assistant. +Goal: Produce a very short, descriptive title (2–4 words) that names the topic of the user's first message. + +Rules: +- Output ONLY the title text. No prefixes, labels, quotes, emojis, hashtags, or trailing punctuation. +- Use the user's language. +- Write a noun phrase that names the topic. Do not write instructions. +- Never output just a pronoun (me/you/I/we/us/myself/yourself). Prefer a neutral subject (e.g., "Assistant", "model", or the concrete topic). +- Never include meta-words: Summarize, Summary, Title, Prompt, Topic, Subject, About, Question, Request, Chat. + +Examples: +User: "Summarize hello" -> Hello +User: "How do I reverse a string in Python?" -> Python string reversal +User: "help me plan a NYC weekend" -> NYC weekend plan +User: "请解释Transformer是如何工作的" -> Transformer 工作原理 +User: "tell me more about you" -> About the assistant +Return only the title text.`, + generateSettings: { + max_tokens: 24, + temperature: 0, + }, + modelId, + locals, + }) + ) + .then((summary) => { + const firstFive = prompt.split(/\s+/g).slice(0, 5).join(" "); + const trimmed = String(summary ?? "").trim(); + // Fallback: if empty, return first five words only (no emoji) + return trimmed || firstFive; + }) + .catch((e) => { + logger.error(e); + const firstFive = prompt.split(/\s+/g).slice(0, 5).join(" "); + return firstFive; + }); +} + +// No post-processing: rely solely on prompt instructions above diff --git a/src/lib/server/textGeneration/types.ts b/src/lib/server/textGeneration/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..791251510c7ef15598d34bab35d0274d779a4e47 --- /dev/null +++ b/src/lib/server/textGeneration/types.ts @@ -0,0 +1,20 @@ +import type { ProcessedModel } from "../models"; +import type { Endpoint } from "../endpoints/endpoints"; +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import type { Assistant } from "$lib/types/Assistant"; + +export interface TextGenerationContext { + model: ProcessedModel; + endpoint: Endpoint; + conv: Conversation; + messages: Message[]; + assistant?: Pick; + promptedAt: Date; + ip: string; + username?: string; + /** Force-enable multimodal handling for endpoints that support it */ + forceMultimodal?: boolean; + locals: App.Locals | undefined; + abortController: AbortController; +} diff --git a/src/lib/server/usageLimits.ts b/src/lib/server/usageLimits.ts new file mode 100644 index 0000000000000000000000000000000000000000..12d46bb2cad4ff641c84b3fdcbfb4baf3dad05c7 --- /dev/null +++ b/src/lib/server/usageLimits.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import { config } from "$lib/server/config"; +import JSON5 from "json5"; + +const sanitizeJSONEnv = (val: string, fallback: string) => { + const raw = (val ?? "").trim(); + const unquoted = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw; + return unquoted || fallback; +}; + +// RATE_LIMIT is the legacy way to define messages per minute limit +export const usageLimitsSchema = z + .object({ + conversations: z.coerce.number().optional(), // how many conversations + messages: z.coerce.number().optional(), // how many messages in a conversation + messageLength: z.coerce.number().optional(), // how long can a message be before we cut it off + messagesPerMinute: z + .preprocess((val) => { + if (val === undefined) { + return config.RATE_LIMIT; + } + return val; + }, z.coerce.number().optional()) + .optional(), // how many messages per minute + }) + .optional(); + +export const usageLimits = usageLimitsSchema.parse( + JSON5.parse(sanitizeJSONEnv(config.USAGE_LIMITS, "{}")) +); diff --git a/src/lib/stores/backgroundGenerations.svelte.ts b/src/lib/stores/backgroundGenerations.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..975435ce9a1d3c28b600a36b195286a9e9405143 --- /dev/null +++ b/src/lib/stores/backgroundGenerations.svelte.ts @@ -0,0 +1,32 @@ +export type BackgroundGeneration = { + id: string; + startedAt: number; +}; + +export const backgroundGenerationEntries = $state([]); + +export function addBackgroundGeneration(entry: BackgroundGeneration) { + const index = backgroundGenerationEntries.findIndex(({ id }) => id === entry.id); + + if (index === -1) { + backgroundGenerationEntries.push(entry); + return; + } + + backgroundGenerationEntries[index] = entry; +} + +export function removeBackgroundGeneration(id: string) { + const index = backgroundGenerationEntries.findIndex((entry) => entry.id === id); + if (index === -1) return; + + backgroundGenerationEntries.splice(index, 1); +} + +export function clearBackgroundGenerations() { + backgroundGenerationEntries.length = 0; +} + +export function hasBackgroundGeneration(id: string) { + return backgroundGenerationEntries.some((entry) => entry.id === id); +} diff --git a/src/lib/stores/backgroundGenerations.ts b/src/lib/stores/backgroundGenerations.ts new file mode 100644 index 0000000000000000000000000000000000000000..442122951cda5d0edeec7c4874f205cab22dbca7 --- /dev/null +++ b/src/lib/stores/backgroundGenerations.ts @@ -0,0 +1 @@ +export * from "./backgroundGenerations.svelte"; diff --git a/src/lib/stores/errors.ts b/src/lib/stores/errors.ts new file mode 100644 index 0000000000000000000000000000000000000000..1022773bd97c122a46be0fc3f9e13c777ad2a04e --- /dev/null +++ b/src/lib/stores/errors.ts @@ -0,0 +1,9 @@ +import { writable } from "svelte/store"; + +export const ERROR_MESSAGES = { + default: "Oops, something went wrong.", + authOnly: "You have to be logged in.", + rateLimited: "You are sending too many messages. Try again later.", +}; + +export const error = writable(undefined); diff --git a/src/lib/stores/isAborted.ts b/src/lib/stores/isAborted.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed24aad14738e1d37e163f46eb8af7fa0fba004a --- /dev/null +++ b/src/lib/stores/isAborted.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export const isAborted = writable(false); diff --git a/src/lib/stores/loading.ts b/src/lib/stores/loading.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4af6918dc6f81ec0ff06d3df083a037981ddd13 --- /dev/null +++ b/src/lib/stores/loading.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export const loading = writable(false); diff --git a/src/lib/stores/pendingMessage.ts b/src/lib/stores/pendingMessage.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a7387f393faac5c219d65880f9517b8ee3d6853 --- /dev/null +++ b/src/lib/stores/pendingMessage.ts @@ -0,0 +1,9 @@ +import { writable } from "svelte/store"; + +export const pendingMessage = writable< + | { + content: string; + files: File[]; + } + | undefined +>(); diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..450b2ad4b0ddcd6f2a3bdbce1f8ab9f41c718071 --- /dev/null +++ b/src/lib/stores/settings.ts @@ -0,0 +1,169 @@ +import { browser } from "$app/environment"; +import { invalidate } from "$app/navigation"; +import { base } from "$app/paths"; +import { UrlDependency } from "$lib/types/UrlDependency"; +import { getContext, setContext } from "svelte"; +import { type Writable, writable, get } from "svelte/store"; + +type SettingsStore = { + shareConversationsWithModelAuthors: boolean; + welcomeModalSeen: boolean; + welcomeModalSeenAt: Date | null; + activeModel: string; + customPrompts: Record; + multimodalOverrides: Record; + recentlySaved: boolean; + disableStream: boolean; + directPaste: boolean; + hidePromptExamples: Record; +}; + +type SettingsStoreWritable = Writable & { + instantSet: (settings: Partial) => Promise; + initValue: ( + key: K, + nestedKey: string, + value: string | boolean + ) => Promise; +}; + +export function useSettingsStore() { + return getContext("settings"); +} + +export function createSettingsStore(initialValue: Omit) { + const baseStore = writable({ ...initialValue, recentlySaved: false }); + + let timeoutId: NodeJS.Timeout; + let showSavedOnNextSync = false; + + async function setSettings(settings: Partial) { + baseStore.update((s) => ({ + ...s, + ...settings, + })); + + if (browser) { + showSavedOnNextSync = true; // User edit, should show "Saved" + clearTimeout(timeoutId); + timeoutId = setTimeout(async () => { + await fetch(`${base}/settings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(get(baseStore)), + }); + + invalidate(UrlDependency.ConversationList); + + if (showSavedOnNextSync) { + // set savedRecently to true for 3s + baseStore.update((s) => ({ + ...s, + recentlySaved: true, + })); + setTimeout(() => { + baseStore.update((s) => ({ + ...s, + recentlySaved: false, + })); + }, 3000); + } + + showSavedOnNextSync = false; + }, 300); + // debounce server calls by 300ms + } + } + + async function initValue( + key: K, + nestedKey: string, + value: string | boolean + ) { + const currentStore = get(baseStore); + const currentNestedObject = currentStore[key] as Record; + + // Only initialize if undefined + if (currentNestedObject?.[nestedKey] !== undefined) { + return; + } + + // Update the store + const newNestedObject = { + ...(currentNestedObject || {}), + [nestedKey]: value, + }; + + baseStore.update((s) => ({ + ...s, + [key]: newNestedObject, + })); + + // Save to server (debounced) - note: we don't set showSavedOnNextSync + if (browser) { + clearTimeout(timeoutId); + timeoutId = setTimeout(async () => { + await fetch(`${base}/settings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(get(baseStore)), + }); + + invalidate(UrlDependency.ConversationList); + + if (showSavedOnNextSync) { + baseStore.update((s) => ({ + ...s, + recentlySaved: true, + })); + setTimeout(() => { + baseStore.update((s) => ({ + ...s, + recentlySaved: false, + })); + }, 3000); + } + + showSavedOnNextSync = false; + }, 300); + } + } + async function instantSet(settings: Partial) { + baseStore.update((s) => ({ + ...s, + ...settings, + })); + + if (browser) { + await fetch(`${base}/settings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...get(baseStore), + ...settings, + }), + }); + invalidate(UrlDependency.ConversationList); + } + } + + const newStore = { + subscribe: baseStore.subscribe, + set: setSettings, + instantSet, + initValue, + update: (fn: (s: SettingsStore) => SettingsStore) => { + setSettings(fn(get(baseStore))); + }, + } satisfies SettingsStoreWritable; + + setContext("settings", newStore); + + return newStore; +} diff --git a/src/lib/stores/shareModal.ts b/src/lib/stores/shareModal.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c3fe0c78b091c0f663676046eee9ee7cd314732 --- /dev/null +++ b/src/lib/stores/shareModal.ts @@ -0,0 +1,13 @@ +import { writable } from "svelte/store"; + +function createShareModalStore() { + const { subscribe, set } = writable(false); + + return { + subscribe, + open: () => set(true), + close: () => set(false), + }; +} + +export const shareModal = createShareModalStore(); diff --git a/src/lib/stores/titleUpdate.ts b/src/lib/stores/titleUpdate.ts new file mode 100644 index 0000000000000000000000000000000000000000..6cefb303e525efe2651ae947a030af129fa01bac --- /dev/null +++ b/src/lib/stores/titleUpdate.ts @@ -0,0 +1,8 @@ +import { writable } from "svelte/store"; + +export interface TitleUpdate { + convId: string; + title: string; +} + +export default writable(null); diff --git a/src/lib/switchTheme.ts b/src/lib/switchTheme.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dac9f669cd26a2549eb1a2f32cde5ef466f03f9 --- /dev/null +++ b/src/lib/switchTheme.ts @@ -0,0 +1,124 @@ +export type ThemePreference = "light" | "dark" | "system"; + +type ThemeState = { + preference: ThemePreference; + isDark: boolean; +}; + +type ThemeSubscriber = (state: ThemeState) => void; + +let currentPreference: ThemePreference = "system"; +const subscribers = new Set(); + +function notify(preference: ThemePreference, isDark: boolean) { + for (const subscriber of subscribers) { + subscriber({ preference, isDark }); + } +} + +export function subscribeToTheme(subscriber: ThemeSubscriber) { + subscribers.add(subscriber); + + if (typeof document !== "undefined") { + const preference = getThemePreference(); + const isDark = document.documentElement.classList.contains("dark"); + subscriber({ preference, isDark }); + } else { + subscriber({ preference: "system", isDark: false }); + } + + return () => { + subscribers.delete(subscriber); + }; +} + +function setMetaThemeColor(isDark: boolean) { + const metaTheme = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement | null; + if (!metaTheme) return; + metaTheme.setAttribute("content", isDark ? "rgb(26, 36, 50)" : "rgb(249, 250, 251)"); +} + +function applyDarkClass(isDark: boolean) { + const { classList } = document.querySelector("html") as HTMLElement; + if (isDark) classList.add("dark"); + else classList.remove("dark"); + setMetaThemeColor(isDark); + notify(currentPreference, isDark); +} + +export function getThemePreference(): ThemePreference { + const raw = typeof localStorage !== "undefined" ? localStorage.getItem("theme") : null; + if (raw === "light" || raw === "dark" || raw === "system") { + currentPreference = raw; + return raw; + } + currentPreference = "system"; + return "system"; +} + +/** + * Explicitly set the theme preference and apply it immediately. + * - "light": force light + * - "dark": force dark + * - "system": follow the OS preference + */ +export function setTheme(preference: ThemePreference) { + try { + localStorage.theme = preference; + } catch (_err) { + void 0; // ignore write errors + } + + const mql = window.matchMedia("(prefers-color-scheme: dark)"); + currentPreference = preference; + const resolve = () => + applyDarkClass(preference === "dark" || (preference === "system" && mql.matches)); + + // Apply now + resolve(); + + // If following system, listen for changes; otherwise remove listener + const listener = () => resolve(); + // Store on window to allow replacing listener later + const key = "__theme_mql_listener" as const; + const w = window as unknown as { + [key: string]: ((this: MediaQueryList, ev: MediaQueryListEvent) => void) | undefined; + }; + const existing = w[key]; + if (existing) { + try { + mql.removeEventListener("change", existing); + } catch (_err) { + // older Safari compatibility + const legacy = ( + mql as unknown as { + removeListener?: (l: (this: MediaQueryList, ev: MediaQueryListEvent) => void) => void; + } + ).removeListener; + legacy?.(existing); + } + w[key] = undefined; + } + if (preference === "system") { + try { + mql.addEventListener("change", listener); + } catch (_err) { + // older Safari compatibility + const legacy = ( + mql as unknown as { + addListener?: (l: (this: MediaQueryList, ev: MediaQueryListEvent) => void) => void; + } + ).addListener; + legacy?.(listener); + } + w[key] = listener; + } +} + +// Backward-compatible toggle used by the sidebar button +export function switchTheme() { + const html = document.querySelector("html") as HTMLElement; + const isDark = html.classList.contains("dark"); + const next: ThemePreference = isDark ? "light" : "dark"; + setTheme(next); +} diff --git a/src/lib/types/AbortedGeneration.ts b/src/lib/types/AbortedGeneration.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe4c2824b4f3257bea71c3acacd65fcee0918188 --- /dev/null +++ b/src/lib/types/AbortedGeneration.ts @@ -0,0 +1,8 @@ +// Ideally shouldn't be needed, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 + +import type { Conversation } from "./Conversation"; +import type { Timestamps } from "./Timestamps"; + +export interface AbortedGeneration extends Timestamps { + conversationId: Conversation["_id"]; +} diff --git a/src/lib/types/Assistant.ts b/src/lib/types/Assistant.ts new file mode 100644 index 0000000000000000000000000000000000000000..c115378be29d8ac0d14b9b6a79df2cc77d5ce490 --- /dev/null +++ b/src/lib/types/Assistant.ts @@ -0,0 +1,31 @@ +import type { ObjectId } from "mongodb"; +import type { User } from "./User"; +import type { Timestamps } from "./Timestamps"; +import type { ReviewStatus } from "./Review"; + +export interface Assistant extends Timestamps { + _id: ObjectId; + createdById: User["_id"] | string; // user id or session + createdByName?: User["username"]; + avatar?: string; + name: string; + description?: string; + modelId: string; + exampleInputs: string[]; + preprompt: string; + userCount?: number; + review: ReviewStatus; + // Web search / RAG removed in this build + generateSettings?: { + temperature?: number; + top_p?: number; + frequency_penalty?: number; + top_k?: number; + }; + dynamicPrompt?: boolean; + searchTokens: string[]; + last24HoursCount: number; +} + +// eslint-disable-next-line no-shadow +// Removed duplicate unused SortKey enum (shared enum exists elsewhere) diff --git a/src/lib/types/AssistantStats.ts b/src/lib/types/AssistantStats.ts new file mode 100644 index 0000000000000000000000000000000000000000..75576c0d7f2413541e431055a09d98ade251cfd7 --- /dev/null +++ b/src/lib/types/AssistantStats.ts @@ -0,0 +1,11 @@ +import type { Timestamps } from "./Timestamps"; +import type { Assistant } from "./Assistant"; + +export interface AssistantStats extends Timestamps { + assistantId: Assistant["_id"]; + date: { + at: Date; + span: "hour"; + }; + count: number; +} diff --git a/src/lib/types/ConfigKey.ts b/src/lib/types/ConfigKey.ts new file mode 100644 index 0000000000000000000000000000000000000000..e76b142b2b22cca1e8da73c07018d8ab71b09da4 --- /dev/null +++ b/src/lib/types/ConfigKey.ts @@ -0,0 +1,4 @@ +export interface ConfigKey { + key: string; // unique + value: string; +} diff --git a/src/lib/types/ConvSidebar.ts b/src/lib/types/ConvSidebar.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbba9abc56d1c0a16cb5c664aa86cf1878a627f1 --- /dev/null +++ b/src/lib/types/ConvSidebar.ts @@ -0,0 +1,9 @@ +import type { ObjectId } from "bson"; + +export interface ConvSidebar { + id: ObjectId | string; + title: string; + updatedAt: Date; + model?: string; + avatarUrl?: string | Promise; +} diff --git a/src/lib/types/Conversation.ts b/src/lib/types/Conversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b9523f7a6f8d122f44d6b304616b2d55fe1994d --- /dev/null +++ b/src/lib/types/Conversation.ts @@ -0,0 +1,27 @@ +import type { ObjectId } from "mongodb"; +import type { Message } from "./Message"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; +import type { Assistant } from "./Assistant"; + +export interface Conversation extends Timestamps { + _id: ObjectId; + + sessionId?: string; + userId?: User["_id"]; + + model: string; + + title: string; + rootMessageId?: Message["id"]; + messages: Message[]; + + meta?: { + fromShareId?: string; + }; + + preprompt?: string; + assistantId?: Assistant["_id"]; + + userAgent?: string; +} diff --git a/src/lib/types/ConversationStats.ts b/src/lib/types/ConversationStats.ts new file mode 100644 index 0000000000000000000000000000000000000000..93b8f1f21b286eef7bb248325c9cfccbf599f693 --- /dev/null +++ b/src/lib/types/ConversationStats.ts @@ -0,0 +1,13 @@ +import type { Timestamps } from "./Timestamps"; + +export interface ConversationStats extends Timestamps { + date: { + at: Date; + span: "day" | "week" | "month"; + field: "updatedAt" | "createdAt"; + }; + type: "conversation" | "message"; + /** _id => number of conversations/messages in the month */ + distinct: "sessionId" | "userId" | "userOrSessionId" | "_id"; + count: number; +} diff --git a/src/lib/types/Message.ts b/src/lib/types/Message.ts new file mode 100644 index 0000000000000000000000000000000000000000..40eb3cd0ee2ae1c5a6ea8af1d8ffc0f4579d5a47 --- /dev/null +++ b/src/lib/types/Message.ts @@ -0,0 +1,39 @@ +import type { InferenceProvider } from "@huggingface/inference"; +import type { MessageUpdate } from "./MessageUpdate"; +import type { Timestamps } from "./Timestamps"; +import type { v4 } from "uuid"; + +export type Message = Partial & { + from: "user" | "assistant" | "system"; + id: ReturnType; + content: string; + updates?: MessageUpdate[]; + + score?: -1 | 0 | 1; + /** + * Either contains the base64 encoded image data + * or the hash of the file stored on the server + **/ + files?: MessageFile[]; + interrupted?: boolean; + + // Router metadata when using llm-router + routerMetadata?: { + route: string; + model: string; + provider?: InferenceProvider; + }; + + // needed for conversation trees + ancestors?: Message["id"][]; + + // goes one level deep + children?: Message["id"][]; +}; + +export type MessageFile = { + type: "hash" | "base64"; + name: string; + value: string; + mime: string; +}; diff --git a/src/lib/types/MessageEvent.ts b/src/lib/types/MessageEvent.ts new file mode 100644 index 0000000000000000000000000000000000000000..edc3cad4eacb495736dbc164ad02f876a95a8eaa --- /dev/null +++ b/src/lib/types/MessageEvent.ts @@ -0,0 +1,10 @@ +import type { Session } from "./Session"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; + +export interface MessageEvent extends Pick { + userId: User["_id"] | Session["sessionId"]; + ip?: string; + expiresAt: Date; + type: "message" | "export"; +} diff --git a/src/lib/types/MessageUpdate.ts b/src/lib/types/MessageUpdate.ts new file mode 100644 index 0000000000000000000000000000000000000000..6400de02b56d9088c15cfac0c51c0f38710425f6 --- /dev/null +++ b/src/lib/types/MessageUpdate.ts @@ -0,0 +1,80 @@ +import type { InferenceProvider } from "@huggingface/inference"; + +export type MessageUpdate = + | MessageStatusUpdate + | MessageTitleUpdate + | MessageStreamUpdate + | MessageFileUpdate + | MessageFinalAnswerUpdate + | MessageReasoningUpdate + | MessageRouterMetadataUpdate; + +export enum MessageUpdateType { + Status = "status", + Title = "title", + Stream = "stream", + File = "file", + FinalAnswer = "finalAnswer", + Reasoning = "reasoning", + RouterMetadata = "routerMetadata", +} + +// Status +export enum MessageUpdateStatus { + Started = "started", + Error = "error", + Finished = "finished", + KeepAlive = "keepAlive", +} +export interface MessageStatusUpdate { + type: MessageUpdateType.Status; + status: MessageUpdateStatus; + message?: string; + statusCode?: number; +} + +// Everything else +export interface MessageTitleUpdate { + type: MessageUpdateType.Title; + title: string; +} +export interface MessageStreamUpdate { + type: MessageUpdateType.Stream; + token: string; +} + +export enum MessageReasoningUpdateType { + Stream = "stream", + Status = "status", +} + +export type MessageReasoningUpdate = MessageReasoningStreamUpdate | MessageReasoningStatusUpdate; + +export interface MessageReasoningStreamUpdate { + type: MessageUpdateType.Reasoning; + subtype: MessageReasoningUpdateType.Stream; + token: string; +} +export interface MessageReasoningStatusUpdate { + type: MessageUpdateType.Reasoning; + subtype: MessageReasoningUpdateType.Status; + status: string; +} + +export interface MessageFileUpdate { + type: MessageUpdateType.File; + name: string; + sha: string; + mime: string; +} +export interface MessageFinalAnswerUpdate { + type: MessageUpdateType.FinalAnswer; + text: string; + interrupted: boolean; +} +export interface MessageRouterMetadataUpdate { + type: MessageUpdateType.RouterMetadata; + route: string; + model: string; + provider?: InferenceProvider; +} diff --git a/src/lib/types/MigrationResult.ts b/src/lib/types/MigrationResult.ts new file mode 100644 index 0000000000000000000000000000000000000000..aff17be6169b247e8dc2a76af164aef58ba9d7d4 --- /dev/null +++ b/src/lib/types/MigrationResult.ts @@ -0,0 +1,7 @@ +import type { ObjectId } from "mongodb"; + +export interface MigrationResult { + _id: ObjectId; + name: string; + status: "success" | "failure" | "ongoing"; +} diff --git a/src/lib/types/Model.ts b/src/lib/types/Model.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c6711d5c0805b192c681a0914a575cae029da2b --- /dev/null +++ b/src/lib/types/Model.ts @@ -0,0 +1,23 @@ +import type { BackendModel } from "$lib/server/models"; + +export type Model = Pick< + BackendModel, + | "id" + | "name" + | "displayName" + | "isRouter" + | "websiteUrl" + | "datasetName" + | "promptExamples" + | "parameters" + | "description" + | "logoUrl" + | "modelUrl" + | "datasetUrl" + | "preprompt" + | "multimodal" + | "multimodalAcceptedMimetypes" + | "unlisted" + | "hasInferenceAPI" + | "providers" +>; diff --git a/src/lib/types/Report.ts b/src/lib/types/Report.ts new file mode 100644 index 0000000000000000000000000000000000000000..949f1b129d0b36c6533d1f9633e47fd133ebe7ac --- /dev/null +++ b/src/lib/types/Report.ts @@ -0,0 +1,12 @@ +import type { ObjectId } from "mongodb"; +import type { User } from "./User"; +import type { Assistant } from "./Assistant"; +import type { Timestamps } from "./Timestamps"; + +export interface Report extends Timestamps { + _id: ObjectId; + createdBy: User["_id"] | string; + object: "assistant" | "tool"; + contentId: Assistant["_id"]; + reason?: string; +} diff --git a/src/lib/types/Review.ts b/src/lib/types/Review.ts new file mode 100644 index 0000000000000000000000000000000000000000..48505f8b4541321a249b9db4ed7960373e3cbe7c --- /dev/null +++ b/src/lib/types/Review.ts @@ -0,0 +1,6 @@ +export enum ReviewStatus { + PRIVATE = "PRIVATE", + PENDING = "PENDING", + APPROVED = "APPROVED", + DENIED = "DENIED", +} diff --git a/src/lib/types/Semaphore.ts b/src/lib/types/Semaphore.ts new file mode 100644 index 0000000000000000000000000000000000000000..e23a13248f928dcf5979b4420e27a25bae3c58fc --- /dev/null +++ b/src/lib/types/Semaphore.ts @@ -0,0 +1,19 @@ +import type { Timestamps } from "./Timestamps"; + +export interface Semaphore extends Timestamps { + key: string; + deleteAt: Date; +} + +export enum Semaphores { + CONVERSATION_STATS = "conversation.stats", + CONFIG_UPDATE = "config.update", + MIGRATION = "migration", + TEST_MIGRATION = "test.migration", + /** + * Note this lock name is used as `${Semaphores.OAUTH_TOKEN_REFRESH}:${sessionId}` + * + * not a global lock, but a lock for each session + */ + OAUTH_TOKEN_REFRESH = "oauth.token.refresh", +} diff --git a/src/lib/types/Session.ts b/src/lib/types/Session.ts new file mode 100644 index 0000000000000000000000000000000000000000..8bba6b9422a74b73fb7bb746f0f734d48fec3dde --- /dev/null +++ b/src/lib/types/Session.ts @@ -0,0 +1,22 @@ +import type { ObjectId } from "bson"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; + +export interface Session extends Timestamps { + _id: ObjectId; + sessionId: string; + userId: User["_id"]; + userAgent?: string; + ip?: string; + expiresAt: Date; + admin?: boolean; + coupledCookieHash?: string; + + oauth?: { + token: { + value: string; + expiresAt: Date; + }; + refreshToken?: string; + }; +} diff --git a/src/lib/types/Settings.ts b/src/lib/types/Settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..d988ca8c8d836074b12f6be6a979e2f8c26225d5 --- /dev/null +++ b/src/lib/types/Settings.ts @@ -0,0 +1,44 @@ +import { defaultModel } from "$lib/server/models"; +import type { Timestamps } from "./Timestamps"; +import type { User } from "./User"; + +export interface Settings extends Timestamps { + userId?: User["_id"]; + sessionId?: string; + + shareConversationsWithModelAuthors: boolean; + /** One-time welcome modal acknowledgement */ + welcomeModalSeenAt?: Date | null; + activeModel: string; + + // model name and system prompts + customPrompts?: Record; + + /** + * Per‑model overrides to enable multimodal (image) support + * even when not advertised by the provider/model list. + * Only the `true` value is meaningful (enables images). + */ + multimodalOverrides?: Record; + + /** + * Per-model toggle to hide Omni prompt suggestions shown near the composer. + * When set to `true`, prompt examples for that model are suppressed. + */ + hidePromptExamples?: Record; + + disableStream: boolean; + directPaste: boolean; +} + +export type SettingsEditable = Omit; +// TODO: move this to a constant file along with other constants +export const DEFAULT_SETTINGS = { + shareConversationsWithModelAuthors: true, + activeModel: defaultModel.id, + customPrompts: {}, + multimodalOverrides: {}, + hidePromptExamples: {}, + disableStream: false, + directPaste: false, +} satisfies SettingsEditable; diff --git a/src/lib/types/SharedConversation.ts b/src/lib/types/SharedConversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..021c1860fbda42c8e99570075108398aac19079f --- /dev/null +++ b/src/lib/types/SharedConversation.ts @@ -0,0 +1,9 @@ +import type { Conversation } from "./Conversation"; + +export type SharedConversation = Pick< + Conversation, + "model" | "title" | "rootMessageId" | "messages" | "preprompt" | "createdAt" | "updatedAt" +> & { + _id: string; + hash: string; +}; diff --git a/src/lib/types/Template.ts b/src/lib/types/Template.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1680e75832c7a4d483c506fd0c0ba9253a1dc18 --- /dev/null +++ b/src/lib/types/Template.ts @@ -0,0 +1,6 @@ +import type { Message } from "./Message"; + +export type ChatTemplateInput = { + messages: Pick[]; + preprompt?: string; +}; diff --git a/src/lib/types/Timestamps.ts b/src/lib/types/Timestamps.ts new file mode 100644 index 0000000000000000000000000000000000000000..12d1867d1be509310190df09d2392bfaa77d6500 --- /dev/null +++ b/src/lib/types/Timestamps.ts @@ -0,0 +1,4 @@ +export interface Timestamps { + createdAt: Date; + updatedAt: Date; +} diff --git a/src/lib/types/TokenCache.ts b/src/lib/types/TokenCache.ts new file mode 100644 index 0000000000000000000000000000000000000000..20c7463b1722437f5b5f7a25ab684c6736e6271a --- /dev/null +++ b/src/lib/types/TokenCache.ts @@ -0,0 +1,6 @@ +import type { Timestamps } from "./Timestamps"; + +export interface TokenCache extends Timestamps { + tokenHash: string; // sha256 of the bearer token + userId: string; // the matching hf user id +} diff --git a/src/lib/types/UrlDependency.ts b/src/lib/types/UrlDependency.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8b901f2ef5bdb90f013133ef50736251da0b78c --- /dev/null +++ b/src/lib/types/UrlDependency.ts @@ -0,0 +1,5 @@ +/* eslint-disable no-shadow */ +export enum UrlDependency { + ConversationList = "conversation:list", + Conversation = "conversation:id", +} diff --git a/src/lib/types/User.ts b/src/lib/types/User.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f300c588885f66835e0ae224539dcc9040a7850 --- /dev/null +++ b/src/lib/types/User.ts @@ -0,0 +1,14 @@ +import type { ObjectId } from "mongodb"; +import type { Timestamps } from "./Timestamps"; + +export interface User extends Timestamps { + _id: ObjectId; + + username?: string; + name: string; + email?: string; + avatarUrl: string | undefined; + hfUserId: string; + isAdmin?: boolean; + isEarlyAccess?: boolean; +} diff --git a/src/lib/utils/PublicConfig.svelte.ts b/src/lib/utils/PublicConfig.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7b118be084fe1e9833358f1ec8ebec237182864 --- /dev/null +++ b/src/lib/utils/PublicConfig.svelte.ts @@ -0,0 +1,77 @@ +import type { env as publicEnv } from "$env/dynamic/public"; +import { page } from "$app/state"; +import { base } from "$app/paths"; + +import type { Transporter } from "@sveltejs/kit"; +import { getContext } from "svelte"; + +type PublicConfigKey = keyof typeof publicEnv; + +class PublicConfigManager { + #configStore = $state>({}); + + constructor(initialConfig?: Record) { + this.init = this.init.bind(this); + this.getPublicConfig = this.getPublicConfig.bind(this); + if (initialConfig) { + this.init(initialConfig); + } + } + + init(publicConfig: Record) { + this.#configStore = publicConfig; + } + + get(key: PublicConfigKey) { + return this.#configStore[key]; + } + + getPublicConfig() { + return this.#configStore; + } + + get isHuggingChat() { + return this.#configStore.PUBLIC_APP_ASSETS === "huggingchat"; + } + + get assetPath() { + return ( + (this.#configStore.PUBLIC_ORIGIN || page.url.origin) + + base + + "/" + + this.#configStore.PUBLIC_APP_ASSETS + ); + } +} +type ConfigProxy = PublicConfigManager & { [K in PublicConfigKey]: string }; + +export function getConfigManager(initialConfig?: Record) { + const publicConfigManager = new PublicConfigManager(initialConfig); + + const publicConfig: ConfigProxy = new Proxy(publicConfigManager, { + get(target, prop) { + if (prop in target) { + return Reflect.get(target, prop); + } + if (typeof prop === "string") { + return target.get(prop as PublicConfigKey); + } + return undefined; + }, + set(target, prop, value, receiver) { + if (prop in target) { + return Reflect.set(target, prop, value, receiver); + } + return false; + }, + }) as ConfigProxy; + return publicConfig; +} + +export const publicConfigTransporter: Transporter = { + encode: (value) => + value instanceof PublicConfigManager ? JSON.stringify(value.getPublicConfig()) : false, + decode: (value) => getConfigManager(JSON.parse(value)), +}; + +export const usePublicConfig = () => getContext("publicConfig"); diff --git a/src/lib/utils/auth.ts b/src/lib/utils/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d79676627f16b3657235c75877af8c9b8c043e6 --- /dev/null +++ b/src/lib/utils/auth.ts @@ -0,0 +1,18 @@ +import { goto } from "$app/navigation"; +import { base } from "$app/paths"; +import { page } from "$app/state"; + +/** + * Redirects to the login page if the user is not authenticated + * and the login feature is enabled. + */ +export function requireAuthUser(): boolean { + if (page.data.loginEnabled && !page.data.user) { + const url = page.data.shared + ? `${base}/login?next=${encodeURIComponent(page.url.pathname + page.url.search)}` + : `${base}/login`; + goto(url, { invalidateAll: true }); + return true; + } + return false; +} diff --git a/src/lib/utils/chunk.ts b/src/lib/utils/chunk.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d8f924eba449978957a62c39c7406f819edf49a --- /dev/null +++ b/src/lib/utils/chunk.ts @@ -0,0 +1,33 @@ +/** + * Chunk array into arrays of length at most `chunkSize` + * + * @param chunkSize must be greater than or equal to 1 + */ +export function chunk(arr: T, chunkSize: number): T[] { + if (isNaN(chunkSize) || chunkSize < 1) { + throw new RangeError("Invalid chunk size: " + chunkSize); + } + + if (!arr.length) { + return []; + } + + /// Small optimization to not chunk buffers unless needed + if (arr.length <= chunkSize) { + return [arr]; + } + + return range(Math.ceil(arr.length / chunkSize)).map((i) => { + return arr.slice(i * chunkSize, (i + 1) * chunkSize); + }) as T[]; +} + +function range(n: number, b?: number): number[] { + return b + ? Array(b - n) + .fill(0) + .map((_, i) => n + i) + : Array(n) + .fill(0) + .map((_, i) => i); +} diff --git a/src/lib/utils/cookiesAreEnabled.ts b/src/lib/utils/cookiesAreEnabled.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5bc92c29335d344c1fde2647c823765f05c4592 --- /dev/null +++ b/src/lib/utils/cookiesAreEnabled.ts @@ -0,0 +1,13 @@ +import { browser } from "$app/environment"; + +export function cookiesAreEnabled(): boolean { + if (!browser) return false; + if (navigator.cookieEnabled) return navigator.cookieEnabled; + + // Create cookie + document.cookie = "cookietest=1"; + const ret = document.cookie.indexOf("cookietest=") != -1; + // Delete cookie + document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + return ret; +} diff --git a/src/lib/utils/debounce.ts b/src/lib/utils/debounce.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8b7560a63e15955496e4a9c425f43504249cdec --- /dev/null +++ b/src/lib/utils/debounce.ts @@ -0,0 +1,17 @@ +/** + * A debounce function that works in both browser and Nodejs. + * For pure Nodejs work, prefer the `Debouncer` class. + */ +export function debounce( + callback: (...rest: T) => unknown, + limit: number +): (...rest: T) => void { + let timer: ReturnType; + + return function (...rest) { + clearTimeout(timer); + timer = setTimeout(() => { + callback(...rest); + }, limit); + }; +} diff --git a/src/lib/utils/deepestChild.ts b/src/lib/utils/deepestChild.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac6ed1d1dd64e6f3a8bbd559887de77b3e49f8c3 --- /dev/null +++ b/src/lib/utils/deepestChild.ts @@ -0,0 +1,6 @@ +export function deepestChild(el: HTMLElement): HTMLElement { + if (el.lastElementChild && el.lastElementChild.nodeType !== Node.TEXT_NODE) { + return deepestChild(el.lastElementChild as HTMLElement); + } + return el; +} diff --git a/src/lib/utils/fetchJSON.ts b/src/lib/utils/fetchJSON.ts new file mode 100644 index 0000000000000000000000000000000000000000..a921046e5a0bdeedff14ca7d323f26addd415934 --- /dev/null +++ b/src/lib/utils/fetchJSON.ts @@ -0,0 +1,23 @@ +export async function fetchJSON( + url: string, + options?: { + fetch?: typeof window.fetch; + allowNull?: boolean; + } +): Promise { + const response = await (options?.fetch ?? fetch)(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); + } + + // Handle empty responses (which parse to null) + const text = await response.text(); + if (!text || text.trim() === "") { + if (options?.allowNull) { + return null as T; + } + throw new Error(`Received empty response from ${url} but allowNull is not set to true`); + } + + return JSON.parse(text); +} diff --git a/src/lib/utils/file2base64.ts b/src/lib/utils/file2base64.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b5dbc66ec3197e19cc3789fda0f0b07a1363b80 --- /dev/null +++ b/src/lib/utils/file2base64.ts @@ -0,0 +1,14 @@ +const file2base64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + const dataUrl = reader.result as string; + const base64 = dataUrl.split(",")[1]; + resolve(base64); + }; + reader.onerror = (error) => reject(error); + }); +}; + +export default file2base64; diff --git a/src/lib/utils/formatUserCount.ts b/src/lib/utils/formatUserCount.ts new file mode 100644 index 0000000000000000000000000000000000000000..27087d7a8ad3e5b36cb2fc800385d27cf3ebdb0a --- /dev/null +++ b/src/lib/utils/formatUserCount.ts @@ -0,0 +1,37 @@ +export function formatUserCount(userCount: number): string { + const userCountRanges: { min: number; max: number; label: string }[] = [ + { min: 0, max: 1, label: "1" }, + { min: 2, max: 9, label: "1-10" }, + { min: 10, max: 49, label: "10+" }, + { min: 50, max: 99, label: "50+" }, + { min: 100, max: 299, label: "100+" }, + { min: 300, max: 499, label: "300+" }, + { min: 500, max: 999, label: "500+" }, + { min: 1_000, max: 2_999, label: "1k+" }, + { min: 3_000, max: 4_999, label: "3k+" }, + { min: 5_000, max: 9_999, label: "5k+" }, + { min: 10_000, max: 19_999, label: "10k+" }, + { min: 20_000, max: 29_999, label: "20k+" }, + { min: 30_000, max: 39_999, label: "30k+" }, + { min: 40_000, max: 49_999, label: "40k+" }, + { min: 50_000, max: 59_999, label: "50k+" }, + { min: 60_000, max: 69_999, label: "60k+" }, + { min: 70_000, max: 79_999, label: "70k+" }, + { min: 80_000, max: 89_999, label: "80k+" }, + { min: 90_000, max: 99_999, label: "90k+" }, + { min: 100_000, max: 109_999, label: "100k+" }, + { min: 110_000, max: 119_999, label: "110k+" }, + { min: 120_000, max: 129_999, label: "120k+" }, + { min: 130_000, max: 139_999, label: "130k+" }, + { min: 140_000, max: 149_999, label: "140k+" }, + { min: 150_000, max: 199_999, label: "150k+" }, + { min: 200_000, max: 299_999, label: "200k+" }, + { min: 300_000, max: 499_999, label: "300k+" }, + { min: 500_000, max: 749_999, label: "500k+" }, + { min: 750_000, max: 999_999, label: "750k+" }, + { min: 1_000_000, max: Infinity, label: "1M+" }, + ]; + + const range = userCountRanges.find(({ min, max }) => userCount >= min && userCount <= max); + return range?.label ?? ""; +} diff --git a/src/lib/utils/getHref.ts b/src/lib/utils/getHref.ts new file mode 100644 index 0000000000000000000000000000000000000000..af5a0a1262520b6cb12c79e81fb3b182ce2cc19d --- /dev/null +++ b/src/lib/utils/getHref.ts @@ -0,0 +1,41 @@ +export function getHref( + url: URL | string, + modifications: { + newKeys?: Record; + existingKeys?: { behaviour: "delete_except" | "delete"; keys: string[] }; + } +) { + const newUrl = new URL(url); + const { newKeys, existingKeys } = modifications; + + // exsiting keys logic + if (existingKeys) { + const { behaviour, keys } = existingKeys; + if (behaviour === "delete") { + for (const key of keys) { + newUrl.searchParams.delete(key); + } + } else { + // delete_except + const keysToPreserve = keys; + for (const key of [...newUrl.searchParams.keys()]) { + if (!keysToPreserve.includes(key)) { + newUrl.searchParams.delete(key); + } + } + } + } + + // new keys logic + if (newKeys) { + for (const [key, val] of Object.entries(newKeys)) { + if (val) { + newUrl.searchParams.set(key, val); + } else { + newUrl.searchParams.delete(key); + } + } + } + + return newUrl.toString(); +} diff --git a/src/lib/utils/getReturnFromGenerator.ts b/src/lib/utils/getReturnFromGenerator.ts new file mode 100644 index 0000000000000000000000000000000000000000..cfb3283cba59064f496f5496a6abab7488ff35f0 --- /dev/null +++ b/src/lib/utils/getReturnFromGenerator.ts @@ -0,0 +1,7 @@ +export async function getReturnFromGenerator(generator: AsyncGenerator): Promise { + let result: IteratorResult; + do { + result = await generator.next(); + } while (!result.done); // Keep calling `next()` until `done` is true + return result.value; // Return the final value +} diff --git a/src/lib/utils/hashConv.ts b/src/lib/utils/hashConv.ts new file mode 100644 index 0000000000000000000000000000000000000000..7231e500bd313877f6df8194a77bf050feec0349 --- /dev/null +++ b/src/lib/utils/hashConv.ts @@ -0,0 +1,12 @@ +import type { Conversation } from "$lib/types/Conversation"; +import { sha256 } from "./sha256"; + +export async function hashConv(conv: Conversation) { + // messages contains the conversation message but only the immutable part + const messages = conv.messages.map((message) => { + return (({ from, id, content }) => ({ from, id, content }))(message); + }); + + const hash = await sha256(JSON.stringify(messages)); + return hash; +} diff --git a/src/lib/utils/isDesktop.ts b/src/lib/utils/isDesktop.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d76f7dca420071cd1385bf8e515efaf64cc7cff --- /dev/null +++ b/src/lib/utils/isDesktop.ts @@ -0,0 +1,7 @@ +// Approximate width from which we disable autofocus +const TABLET_VIEWPORT_WIDTH = 768; + +export function isDesktop(window: Window) { + const { innerWidth } = window; + return innerWidth > TABLET_VIEWPORT_WIDTH; +} diff --git a/src/lib/utils/isUrl.ts b/src/lib/utils/isUrl.ts new file mode 100644 index 0000000000000000000000000000000000000000..d24c0eaa499614a7c5b208bd2c1684e18227bfec --- /dev/null +++ b/src/lib/utils/isUrl.ts @@ -0,0 +1,8 @@ +export function isURL(url: string) { + try { + new URL(url); + return true; + } catch (e) { + return false; + } +} diff --git a/src/lib/utils/isVirtualKeyboard.ts b/src/lib/utils/isVirtualKeyboard.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b331abecd4056adcd226aff189d9ba0624e6cb5 --- /dev/null +++ b/src/lib/utils/isVirtualKeyboard.ts @@ -0,0 +1,16 @@ +import { browser } from "$app/environment"; + +export function isVirtualKeyboard(): boolean { + if (!browser) return false; + + // Check for touch capability + if (navigator.maxTouchPoints > 0 && screen.width <= 768) return true; + + // Check for touch events + if ("ontouchstart" in window) return true; + + // Fallback to user agent string check + const userAgent = navigator.userAgent.toLowerCase(); + + return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); +} diff --git a/src/lib/utils/loadAttachmentsFromUrls.ts b/src/lib/utils/loadAttachmentsFromUrls.ts new file mode 100644 index 0000000000000000000000000000000000000000..c56f2f64ed615c92d3a141501cb220b2e542a520 --- /dev/null +++ b/src/lib/utils/loadAttachmentsFromUrls.ts @@ -0,0 +1,104 @@ +import { base } from "$app/paths"; + +export interface AttachmentLoadResult { + files: File[]; + errors: string[]; +} + +/** + * Parse attachment URLs from query parameters + * Supports both comma-separated (?attachments=url1,url2) and multiple params (?attachments=url1&attachments=url2) + */ +function parseAttachmentUrls(searchParams: URLSearchParams): string[] { + const urls: string[] = []; + + // Get all 'attachments' parameters + const attachmentParams = searchParams.getAll("attachments"); + + for (const param of attachmentParams) { + // Split by comma in case multiple URLs are in one param + const splitUrls = param.split(",").map((url) => url.trim()); + urls.push(...splitUrls); + } + + // Filter out empty strings + return urls.filter((url) => url.length > 0); +} + +/** + * Extract filename from URL or Content-Disposition header + */ +function extractFilename(url: string, contentDisposition?: string | null): string { + // Try to get filename from Content-Disposition header + if (contentDisposition) { + const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (match && match[1]) { + return match[1].replace(/['"]/g, ""); + } + } + + // Fallback: extract from URL + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const segments = pathname.split("/"); + const lastSegment = segments[segments.length - 1]; + + if (lastSegment && lastSegment.length > 0) { + return decodeURIComponent(lastSegment); + } + } catch { + // Invalid URL, fall through to default + } + + return "attachment"; +} + +/** + * Load files from remote URLs via server-side proxy + */ +export async function loadAttachmentsFromUrls( + searchParams: URLSearchParams +): Promise { + const urls = parseAttachmentUrls(searchParams); + + if (urls.length === 0) { + return { files: [], errors: [] }; + } + + const files: File[] = []; + const errors: string[] = []; + + await Promise.all( + urls.map(async (url) => { + try { + // Fetch via our proxy endpoint to bypass CORS + const proxyUrl = `${base}/api/fetch-url?${new URLSearchParams({ url })}`; + const response = await fetch(proxyUrl); + + if (!response.ok) { + const errorText = await response.text(); + errors.push(`Failed to fetch ${url}: ${errorText}`); + return; + } + + const blob = await response.blob(); + const contentDisposition = response.headers.get("content-disposition"); + const filename = extractFilename(url, contentDisposition); + + // Create File object + const file = new File([blob], filename, { + type: blob.type || "application/octet-stream", + }); + + files.push(file); + } catch (err) { + const message = err instanceof Error ? err.message : "Unknown error"; + errors.push(`Failed to load ${url}: ${message}`); + console.error(`Error loading attachment from ${url}:`, err); + } + }) + ); + + return { files, errors }; +} diff --git a/src/lib/utils/marked.ts b/src/lib/utils/marked.ts new file mode 100644 index 0000000000000000000000000000000000000000..86149a0451185033d7b1594035b90e9531ab91d8 --- /dev/null +++ b/src/lib/utils/marked.ts @@ -0,0 +1,309 @@ +import katex from "katex"; +import "katex/dist/contrib/mhchem.mjs"; +import { Marked } from "marked"; +import type { Tokens, TokenizerExtension, RendererExtension } from "marked"; +// Simple type to replace removed WebSearchSource +type SimpleSource = { + title?: string; + link: string; +}; +import hljs from "highlight.js"; +import { parseIncompleteMarkdown } from "./parseIncompleteMarkdown"; +import { parseMarkdownIntoBlocks } from "./parseBlocks"; + +interface katexBlockToken extends Tokens.Generic { + type: "katexBlock"; + raw: string; + text: string; + displayMode: true; +} + +interface katexInlineToken extends Tokens.Generic { + type: "katexInline"; + raw: string; + text: string; + displayMode: false; +} + +export const katexBlockExtension: TokenizerExtension & RendererExtension = { + name: "katexBlock", + level: "block", + + start(src: string): number | undefined { + const match = src.match(/(\${2}|\\\[)/); + return match ? match.index : -1; + }, + + tokenizer(src: string): katexBlockToken | undefined { + // 1) $$ ... $$ + const rule1 = /^\${2}([\s\S]+?)\${2}/; + const match1 = rule1.exec(src); + if (match1) { + const token: katexBlockToken = { + type: "katexBlock", + raw: match1[0], + text: match1[1].trim(), + displayMode: true, + }; + return token; + } + + // 2) \[ ... \] + const rule2 = /^\\\[([\s\S]+?)\\\]/; + const match2 = rule2.exec(src); + if (match2) { + const token: katexBlockToken = { + type: "katexBlock", + raw: match2[0], + text: match2[1].trim(), + displayMode: true, + }; + return token; + } + + return undefined; + }, + + renderer(token) { + if (token.type === "katexBlock") { + return katex.renderToString(token.text, { + throwOnError: false, + displayMode: token.displayMode, + }); + } + return undefined; + }, +}; + +const katexInlineExtension: TokenizerExtension & RendererExtension = { + name: "katexInline", + level: "inline", + + start(src: string): number | undefined { + const match = src.match(/(\$|\\\()/); + return match ? match.index : -1; + }, + + tokenizer(src: string): katexInlineToken | undefined { + // 1) $...$ + const rule1 = /^\$([^$]+?)\$/; + const match1 = rule1.exec(src); + if (match1) { + const token: katexInlineToken = { + type: "katexInline", + raw: match1[0], + text: match1[1].trim(), + displayMode: false, + }; + return token; + } + + // 2) \(...\) + const rule2 = /^\\\(([\s\S]+?)\\\)/; + const match2 = rule2.exec(src); + if (match2) { + const token: katexInlineToken = { + type: "katexInline", + raw: match2[0], + text: match2[1].trim(), + displayMode: false, + }; + return token; + } + + return undefined; + }, + + renderer(token) { + if (token.type === "katexInline") { + return katex.renderToString(token.text, { + throwOnError: false, + displayMode: token.displayMode, + }); + } + return undefined; + }, +}; + +function escapeHTML(content: string) { + return content.replace( + /[<>&"']/g, + (x) => + ({ + "<": "<", + ">": ">", + "&": "&", + "'": "'", + '"': """, + })[x] || x + ); +} + +function addInlineCitations(md: string, webSearchSources: SimpleSource[] = []): string { + const linkStyle = + "color: rgb(59, 130, 246); text-decoration: none; hover:text-decoration: underline;"; + return md.replace(/\[(\d+)\]/g, (match: string) => { + const indices: number[] = (match.match(/\d+/g) || []).map(Number); + const links: string = indices + .map((index: number) => { + if (index === 0) return false; + const source = webSearchSources[index - 1]; + if (source) { + return `${index}`; + } + return ""; + }) + .filter(Boolean) + .join(", "); + return links ? ` ${links}` : match; + }); +} + +function createMarkedInstance(sources: SimpleSource[]): Marked { + return new Marked({ + hooks: { + postprocess: (html) => addInlineCitations(html, sources), + }, + extensions: [katexBlockExtension, katexInlineExtension], + renderer: { + link: (href, title, text) => + `${text}`, + html: (html) => escapeHTML(html), + }, + gfm: true, + breaks: true, + }); +} +function isFencedBlockClosed(raw?: string): boolean { + if (!raw) return true; + /* eslint-disable-next-line no-control-regex */ + const trimmed = raw.replace(/[\s\u0000]+$/, ""); + const openingFenceMatch = trimmed.match(/^([`~]{3,})/); + if (!openingFenceMatch) { + return true; + } + const fence = openingFenceMatch[1]; + const closingFencePattern = new RegExp(`(?:\n|\r\n)${fence}(?:[\t ]+)?$`); + return closingFencePattern.test(trimmed); +} + +type CodeToken = { + type: "code"; + lang: string; + code: string; + rawCode: string; + isClosed: boolean; +}; + +type TextToken = { + type: "text"; + html: string | Promise; +}; + +export async function processTokens(content: string, sources: SimpleSource[]): Promise { + // Apply incomplete markdown preprocessing for smooth streaming + const processedContent = parseIncompleteMarkdown(content); + + const marked = createMarkedInstance(sources); + const tokens = marked.lexer(processedContent); + + const processedTokens = await Promise.all( + tokens.map(async (token) => { + if (token.type === "code") { + return { + type: "code" as const, + lang: token.lang, + code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value, + rawCode: token.text, + isClosed: isFencedBlockClosed(token.raw ?? ""), + }; + } else { + return { + type: "text" as const, + html: marked.parse(token.raw), + }; + } + }) + ); + + return processedTokens; +} + +export function processTokensSync(content: string, sources: SimpleSource[]): Token[] { + // Apply incomplete markdown preprocessing for smooth streaming + const processedContent = parseIncompleteMarkdown(content); + + const marked = createMarkedInstance(sources); + const tokens = marked.lexer(processedContent); + return tokens.map((token) => { + if (token.type === "code") { + return { + type: "code" as const, + lang: token.lang, + code: hljs.highlightAuto(token.text, hljs.getLanguage(token.lang)?.aliases).value, + rawCode: token.text, + isClosed: isFencedBlockClosed(token.raw ?? ""), + }; + } + return { type: "text" as const, html: marked.parse(token.raw) }; + }); +} + +export type Token = CodeToken | TextToken; + +export type BlockToken = { + id: string; + content: string; + tokens: Token[]; +}; + +/** + * Simple hash function for generating stable block IDs + */ +function hashString(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash).toString(36); +} + +/** + * Process markdown content into blocks with stable IDs for efficient memoization. + * Each block is processed independently and assigned a content-based hash ID. + */ +export async function processBlocks( + content: string, + sources: SimpleSource[] = [] +): Promise { + const blocks = parseMarkdownIntoBlocks(content); + + return await Promise.all( + blocks.map(async (blockContent, index) => { + const tokens = await processTokens(blockContent, sources); + return { + id: `${index}-${hashString(blockContent)}`, + content: blockContent, + tokens, + }; + }) + ); +} + +/** + * Synchronous version of processBlocks for SSR + */ +export function processBlocksSync(content: string, sources: SimpleSource[] = []): BlockToken[] { + const blocks = parseMarkdownIntoBlocks(content); + + return blocks.map((blockContent, index) => { + const tokens = processTokensSync(blockContent, sources); + return { + id: `${index}-${hashString(blockContent)}`, + content: blockContent, + tokens, + }; + }); +} diff --git a/src/lib/utils/mergeAsyncGenerators.ts b/src/lib/utils/mergeAsyncGenerators.ts new file mode 100644 index 0000000000000000000000000000000000000000..08544298c69d75f638daaf30c14084d106dd6009 --- /dev/null +++ b/src/lib/utils/mergeAsyncGenerators.ts @@ -0,0 +1,38 @@ +type Gen = AsyncGenerator; + +type GenPromiseMap = Map< + Gen, + Promise<{ gen: Gen } & IteratorResult> +>; + +/** Merges multiple async generators into a single async generator that yields values from all of them in parallel. */ +export async function* mergeAsyncGenerators( + generators: Gen[] +): Gen { + const promises: GenPromiseMap = new Map(); + const results: Map, TReturn> = new Map(); + + for (const gen of generators) { + promises.set( + gen, + gen.next().then((result) => ({ gen, ...result })) + ); + } + + while (promises.size) { + const { gen, value, done } = await Promise.race(promises.values()); + if (done) { + results.set(gen, value as TReturn); + promises.delete(gen); + } else { + promises.set( + gen, + gen.next().then((result) => ({ gen, ...result })) + ); + yield value as T; + } + } + + const orderedResults = generators.map((gen) => results.get(gen) as TReturn); + return orderedResults; +} diff --git a/src/lib/utils/messageUpdates.ts b/src/lib/utils/messageUpdates.ts new file mode 100644 index 0000000000000000000000000000000000000000..c72fc0e514a63efebf0501a2033135c66cfc3df8 --- /dev/null +++ b/src/lib/utils/messageUpdates.ts @@ -0,0 +1,232 @@ +import type { MessageFile } from "$lib/types/Message"; +import { + type MessageUpdate, + type MessageStreamUpdate, + MessageUpdateType, +} from "$lib/types/MessageUpdate"; + +import { page } from "$app/state"; + +type MessageUpdateRequestOptions = { + base: string; + inputs?: string; + messageId?: string; + isRetry: boolean; + files?: MessageFile[]; +}; +export async function fetchMessageUpdates( + conversationId: string, + opts: MessageUpdateRequestOptions, + abortSignal: AbortSignal +): Promise> { + const abortController = new AbortController(); + abortSignal.addEventListener("abort", () => abortController.abort()); + + const form = new FormData(); + + const optsJSON = JSON.stringify({ + inputs: opts.inputs, + id: opts.messageId, + is_retry: opts.isRetry, + }); + + opts.files?.forEach((file) => { + const name = file.type + ";" + file.name; + + form.append("files", new File([file.value], name, { type: file.mime })); + }); + + form.append("data", optsJSON); + + const response = await fetch(`${opts.base}/conversation/${conversationId}`, { + method: "POST", + body: form, + signal: abortController.signal, + }); + + if (!response.ok) { + const errorMessage = await response + .json() + .then((obj) => obj.message) + .catch(() => `Request failed with status code ${response.status}: ${response.statusText}`); + throw Error(errorMessage); + } + if (!response.body) { + throw Error("Body not defined"); + } + + if (!(page.data.publicConfig.PUBLIC_SMOOTH_UPDATES === "true")) { + return endpointStreamToIterator(response, abortController); + } + + return smoothAsyncIterator( + streamMessageUpdatesToFullWords(endpointStreamToIterator(response, abortController)) + ); +} + +async function* endpointStreamToIterator( + response: Response, + abortController: AbortController +): AsyncGenerator { + const reader = response.body?.pipeThrough(new TextDecoderStream()).getReader(); + if (!reader) throw Error("Response for endpoint had no body"); + + // Handle any cases where we must abort + reader.closed.then(() => abortController.abort()); + + // Handle logic for aborting + abortController.signal.addEventListener("abort", () => reader.cancel()); + + // ex) If the last response is => {"type": "stream", "token": + // It should be => {"type": "stream", "token": "Hello"} = prev_input_chunk + "Hello"} + let prevChunk = ""; + while (!abortController.signal.aborted) { + const { done, value } = await reader.read(); + if (done) { + abortController.abort(); + break; + } + if (!value) continue; + + const { messageUpdates, remainingText } = parseMessageUpdates(prevChunk + value); + prevChunk = remainingText; + for (const messageUpdate of messageUpdates) yield messageUpdate; + } +} + +function parseMessageUpdates(value: string): { + messageUpdates: MessageUpdate[]; + remainingText: string; +} { + const inputs = value.split("\n"); + const messageUpdates: MessageUpdate[] = []; + for (const input of inputs) { + try { + messageUpdates.push(JSON.parse(input) as MessageUpdate); + } catch (error) { + // in case of parsing error, we return what we were able to parse + if (error instanceof SyntaxError) { + return { + messageUpdates, + remainingText: inputs.at(-1) ?? "", + }; + } + } + } + return { messageUpdates, remainingText: "" }; +} + +/** + * Emits all the message updates immediately that aren't "stream" type + * Emits a concatenated "stream" type message update once it detects a full word + * Example: "what" " don" "'t" => "what" " don't" + * Only supports latin languages, ignores others + */ +async function* streamMessageUpdatesToFullWords( + iterator: AsyncGenerator +): AsyncGenerator { + let bufferedStreamUpdates: MessageStreamUpdate[] = []; + + const endAlphanumeric = /[a-zA-Z0-9À-ž'`]+$/; + const beginnningAlphanumeric = /^[a-zA-Z0-9À-ž'`]+/; + + for await (const messageUpdate of iterator) { + if (messageUpdate.type !== "stream") { + yield messageUpdate; + continue; + } + bufferedStreamUpdates.push(messageUpdate); + + let lastIndexEmitted = 0; + for (let i = 1; i < bufferedStreamUpdates.length; i++) { + const prevEndsAlphanumeric = endAlphanumeric.test(bufferedStreamUpdates[i - 1].token); + const currBeginsAlphanumeric = beginnningAlphanumeric.test(bufferedStreamUpdates[i].token); + const shouldCombine = prevEndsAlphanumeric && currBeginsAlphanumeric; + const combinedTooMany = i - lastIndexEmitted >= 5; + if (shouldCombine && !combinedTooMany) continue; + + // Combine tokens together and emit + yield { + type: MessageUpdateType.Stream, + token: bufferedStreamUpdates + .slice(lastIndexEmitted, i) + .map((_) => _.token) + .join(""), + }; + lastIndexEmitted = i; + } + bufferedStreamUpdates = bufferedStreamUpdates.slice(lastIndexEmitted); + } + for (const messageUpdate of bufferedStreamUpdates) yield messageUpdate; +} + +/** + * Attempts to smooth out the time between values emitted by an async iterator + * by waiting for the average time between values to emit the next value + */ +async function* smoothAsyncIterator(iterator: AsyncGenerator): AsyncGenerator { + const eventTarget = new EventTarget(); + let done = false; + const valuesBuffer: T[] = []; + const valueTimesMS: number[] = []; + + const next = async () => { + const obj = await iterator.next(); + if (obj.done) { + done = true; + } else { + valuesBuffer.push(obj.value); + valueTimesMS.push(performance.now()); + next(); + } + eventTarget.dispatchEvent(new Event("next")); + }; + next(); + + let timeOfLastEmitMS = performance.now(); + while (!done || valuesBuffer.length > 0) { + // Only consider the last X times between tokens + const sampledTimesMS = valueTimesMS.slice(-30); + + // Get the total time spent in abnormal periods + const anomalyThresholdMS = 2000; + const anomalyDurationMS = sampledTimesMS + .map((time, i, times) => time - times[i - 1]) + .slice(1) + .filter((time) => time > anomalyThresholdMS) + .reduce((a, b) => a + b, 0); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const totalTimeMSBetweenValues = sampledTimesMS.at(-1)! - sampledTimesMS[0]; + const timeMSBetweenValues = totalTimeMSBetweenValues - anomalyDurationMS; + + const averageTimeMSBetweenValues = Math.min( + 200, + timeMSBetweenValues / (sampledTimesMS.length - 1) + ); + const timeSinceLastEmitMS = performance.now() - timeOfLastEmitMS; + + // Emit after waiting duration or cancel if "next" event is emitted + const gotNext = await Promise.race([ + sleep(Math.max(5, averageTimeMSBetweenValues - timeSinceLastEmitMS)), + waitForEvent(eventTarget, "next"), + ]); + + // Go to next iteration so we can re-calculate when to emit + if (gotNext) continue; + + // Nothing in buffer to emit + if (valuesBuffer.length === 0) continue; + + // Emit + timeOfLastEmitMS = performance.now(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + yield valuesBuffer.shift()!; + } +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const waitForEvent = (eventTarget: EventTarget, eventName: string) => + new Promise((resolve) => + eventTarget.addEventListener(eventName, () => resolve(true), { once: true }) + ); diff --git a/src/lib/utils/models.ts b/src/lib/utils/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..f98a49ae6fda6c85f7a0a1ffa49b085aa25a64fb --- /dev/null +++ b/src/lib/utils/models.ts @@ -0,0 +1,14 @@ +import type { Model } from "$lib/types/Model"; + +export const findCurrentModel = ( + models: Model[], + _oldModels: { id: string; transferTo?: string }[] = [], + id?: string +): Model => { + if (id) { + const direct = models.find((m) => m.id === id); + if (direct) return direct; + } + + return models[0]; +}; diff --git a/src/lib/utils/parseBlocks.ts b/src/lib/utils/parseBlocks.ts new file mode 100644 index 0000000000000000000000000000000000000000..d891d0cc13e021b7463307401176d269df19c8a3 --- /dev/null +++ b/src/lib/utils/parseBlocks.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 Vercel, Inc. + * Adapted from: https://github.com/vercel/streamdown/blob/main/packages/streamdown/lib/parse-blocks.tsx + */ + +import { Lexer } from "marked"; + +/** + * Parses markdown into independent blocks for efficient memoization during streaming. + * Blocks are split at natural boundaries while keeping related content together. + */ +export function parseMarkdownIntoBlocks(markdown: string): string[] { + // Check if the markdown contains footnotes (references or definitions) + // Footnote references: [^1], [^label], etc. + // Footnote definitions: [^1]: text, [^label]: text, etc. + // Use atomic groups or possessive quantifiers to prevent backtracking + const hasFootnoteReference = /\[\^[^\]\s]{1,200}\](?!:)/.test(markdown); + const hasFootnoteDefinition = /\[\^[^\]\s]{1,200}\]:/.test(markdown); + + // If footnotes are present, return the entire document as a single block + // This ensures footnote references and definitions remain in the same mdast tree + if (hasFootnoteReference || hasFootnoteDefinition) { + return [markdown]; + } + + const tokens = Lexer.lex(markdown, { gfm: true }); + + // Post-process to merge consecutive blocks that belong together + const mergedBlocks: string[] = []; + const htmlStack: string[] = []; // Track opening HTML tags + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + const currentBlock = token.raw; + + // Check if we're inside an HTML block + if (htmlStack.length > 0) { + // We're inside an HTML block, merge with the previous block + mergedBlocks[mergedBlocks.length - 1] += currentBlock; + + // Check if this token closes an HTML tag + if (token.type === "html") { + const closingTagMatch = currentBlock.match(/<\/(\w+)>/); + if (closingTagMatch) { + const closingTag = closingTagMatch[1]; + // Check if this closes the most recent opening tag + if (htmlStack[htmlStack.length - 1] === closingTag) { + htmlStack.pop(); + } + } + } + continue; + } + + // Check if this is an opening HTML block tag + if (token.type === "html" && token.block) { + const openingTagMatch = currentBlock.match(/<(\w+)[\s>]/); + if (openingTagMatch) { + const tagName = openingTagMatch[1]; + // Check if this is a self-closing tag or if there's a closing tag in the same block + const hasClosingTag = currentBlock.includes(``); + if (!hasClosingTag) { + // This is an opening tag without a closing tag in the same block + htmlStack.push(tagName); + } + } + } + + // Math block merging logic (existing) + // Check if this is a standalone $$ that might be a closing delimiter + if (currentBlock.trim() === "$$" && mergedBlocks.length > 0) { + const previousBlock = mergedBlocks.at(-1); + + if (!previousBlock) { + mergedBlocks.push(currentBlock); + continue; + } + + // Check if the previous block starts with $$ but doesn't end with $$ + const prevStartsWith$$ = previousBlock.trimStart().startsWith("$$"); + const prevDollarCount = (previousBlock.match(/\$\$/g) || []).length; + + // If previous block has odd number of $$ and starts with $$, merge them + if (prevStartsWith$$ && prevDollarCount % 2 === 1) { + mergedBlocks[mergedBlocks.length - 1] = previousBlock + currentBlock; + continue; + } + } + + // Check if current block ends with $$ and previous block started with $$ but didn't close + if (mergedBlocks.length > 0 && currentBlock.trimEnd().endsWith("$$")) { + const previousBlock = mergedBlocks.at(-1); + + if (!previousBlock) { + mergedBlocks.push(currentBlock); + continue; + } + + const prevStartsWith$$ = previousBlock.trimStart().startsWith("$$"); + const prevDollarCount = (previousBlock.match(/\$\$/g) || []).length; + const currDollarCount = (currentBlock.match(/\$\$/g) || []).length; + + // If previous block has unclosed math (odd $$) and current block ends with $$ + // AND current block doesn't start with $$, it's likely a continuation + if ( + prevStartsWith$$ && + prevDollarCount % 2 === 1 && + !currentBlock.trimStart().startsWith("$$") && + currDollarCount === 1 + ) { + mergedBlocks[mergedBlocks.length - 1] = previousBlock + currentBlock; + continue; + } + } + + mergedBlocks.push(currentBlock); + } + + return mergedBlocks; +} diff --git a/src/lib/utils/parseIncompleteMarkdown.ts b/src/lib/utils/parseIncompleteMarkdown.ts new file mode 100644 index 0000000000000000000000000000000000000000..7da8e77ec01d1e30c404b29076721aabec7ddbb2 --- /dev/null +++ b/src/lib/utils/parseIncompleteMarkdown.ts @@ -0,0 +1,644 @@ +/* + * Copyright 2023 Vercel, Inc. + * Source: https://github.com/vercel/streamdown/blob/main/packages/streamdown/lib/parse-incomplete-markdown.ts + */ + +const linkImagePattern = /(!?\[)([^\]]*?)$/; +const boldPattern = /(\*\*)([^*]*?)$/; +const italicPattern = /(__)([^_]*?)$/; +const boldItalicPattern = /(\*\*\*)([^*]*?)$/; +const singleAsteriskPattern = /(\*)([^*]*?)$/; +const singleUnderscorePattern = /(_)([^_]*?)$/; +const inlineCodePattern = /(`)([^`]*?)$/; +const strikethroughPattern = /(~~)([^~]*?)$/; + +// Helper function to check if we have a complete code block +const hasCompleteCodeBlock = (text: string): boolean => { + const tripleBackticks = (text.match(/```/g) || []).length; + return tripleBackticks > 0 && tripleBackticks % 2 === 0 && text.includes("\n"); +}; + +// Returns the start index of the currently open fenced code block, or -1 if none +const getOpenCodeFenceIndex = (text: string): number => { + let openFenceIndex = -1; + let inFence = false; + + for (const match of text.matchAll(/```/g)) { + const index = match.index ?? -1; + if (index === -1) { + continue; + } + + if (inFence) { + // This fence closes the current block + inFence = false; + openFenceIndex = -1; + } else { + // This fence opens a new block + inFence = true; + openFenceIndex = index; + } + } + + return openFenceIndex; +}; + +// Handles incomplete links and images by preserving them with a special marker +const handleIncompleteLinksAndImages = (text: string): string => { + // First check for incomplete URLs: [text](partial-url or ![text](partial-url without closing ) + // Pattern: !?[text](url-without-closing-paren at end of string + const incompleteLinkUrlPattern = /(!?)\[([^\]]+)\]\(([^)]+)$/; + const incompleteLinkUrlMatch = text.match(incompleteLinkUrlPattern); + + if (incompleteLinkUrlMatch) { + const isImage = incompleteLinkUrlMatch[1] === "!"; + const linkText = incompleteLinkUrlMatch[2]; + const partialUrl = incompleteLinkUrlMatch[3]; + + // Find the start position of this link/image pattern + const matchStart = text.lastIndexOf(`${isImage ? "!" : ""}[${linkText}](${partialUrl}`); + const beforeLink = text.substring(0, matchStart); + + if (isImage) { + // For images with incomplete URLs, remove them entirely + return beforeLink; + } + + // For links with incomplete URLs, replace the URL with placeholder and close it + return `${beforeLink}[${linkText}](streamdown:incomplete-link)`; + } + + // Then check for incomplete link text: [partial-text without closing ] + const linkMatch = text.match(linkImagePattern); + + if (linkMatch) { + const isImage = linkMatch[1].startsWith("!"); + + // For images, we still remove them as they can't show skeleton + if (isImage) { + const startIndex = text.lastIndexOf(linkMatch[1]); + return text.substring(0, startIndex); + } + + // For links, preserve the text and close the link with a + // special placeholder URL that indicates it's incomplete + return `${text}](streamdown:incomplete-link)`; + } + + return text; +}; + +// Completes incomplete bold formatting (**) +const handleIncompleteBold = (text: string): string => { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) { + return text; + } + + const boldMatch = text.match(boldPattern); + + if (boldMatch) { + // Don't close if there's no meaningful content after the opening markers + // boldMatch[2] contains the content after ** + // Check if content is only whitespace or other emphasis markers + const contentAfterMarker = boldMatch[2]; + if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) { + return text; + } + + // Check if the bold marker is in a list item context + // Find the position of the matched bold marker + const markerIndex = text.lastIndexOf(boldMatch[1]); + + // Don't process if the marker is inside an incomplete code block + const openFenceIndex = getOpenCodeFenceIndex(text); + if (openFenceIndex !== -1 && markerIndex > openFenceIndex) { + return text; + } + const beforeMarker = text.substring(0, markerIndex); + const lastNewlineBeforeMarker = beforeMarker.lastIndexOf("\n"); + const lineStart = lastNewlineBeforeMarker === -1 ? 0 : lastNewlineBeforeMarker + 1; + const lineBeforeMarker = text.substring(lineStart, markerIndex); + + // Check if this line is a list item with just the bold marker + if (/^[\s]*[-*+][\s]+$/.test(lineBeforeMarker)) { + // This is a list item with just emphasis markers + // Check if content after marker spans multiple lines + const hasNewlineInContent = contentAfterMarker.includes("\n"); + if (hasNewlineInContent) { + // Don't complete if the content spans to another line + return text; + } + } + + const asteriskPairs = (text.match(/\*\*/g) || []).length; + if (asteriskPairs % 2 === 1) { + return `${text}**`; + } + } + + return text; +}; + +// Completes incomplete italic formatting with double underscores (__) +const handleIncompleteDoubleUnderscoreItalic = (text: string): string => { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) { + return text; + } + + const italicMatch = text.match(italicPattern); + + if (italicMatch) { + // Don't close if there's no meaningful content after the opening markers + // italicMatch[2] contains the content after __ + // Check if content is only whitespace or other emphasis markers + const contentAfterMarker = italicMatch[2]; + if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) { + return text; + } + + // Check if the underscore marker is in a list item context + // Find the position of the matched underscore marker + const markerIndex = text.lastIndexOf(italicMatch[1]); + + // Don't process if the marker is inside an incomplete code block + const openFenceIndex = getOpenCodeFenceIndex(text); + if (openFenceIndex !== -1 && markerIndex > openFenceIndex) { + return text; + } + const beforeMarker = text.substring(0, markerIndex); + const lastNewlineBeforeMarker = beforeMarker.lastIndexOf("\n"); + const lineStart = lastNewlineBeforeMarker === -1 ? 0 : lastNewlineBeforeMarker + 1; + const lineBeforeMarker = text.substring(lineStart, markerIndex); + + // Check if this line is a list item with just the underscore marker + if (/^[\s]*[-*+][\s]+$/.test(lineBeforeMarker)) { + // This is a list item with just emphasis markers + // Check if content after marker spans multiple lines + const hasNewlineInContent = contentAfterMarker.includes("\n"); + if (hasNewlineInContent) { + // Don't complete if the content spans to another line + return text; + } + } + + const underscorePairs = (text.match(/__/g) || []).length; + if (underscorePairs % 2 === 1) { + return `${text}__`; + } + } + + return text; +}; + +// Counts single asterisks that are not part of double asterisks, not escaped, and not list markers +const countSingleAsterisks = (text: string): number => { + return text.split("").reduce((acc, char, index) => { + if (char === "*") { + const prevChar = text[index - 1]; + const nextChar = text[index + 1]; + // Skip if escaped with backslash + if (prevChar === "\\") { + return acc; + } + // Check if this is a list marker (asterisk at start of line followed by space) + // Look backwards to find the start of the current line + let lineStartIndex = index; + for (let i = index - 1; i >= 0; i--) { + if (text[i] === "\n") { + lineStartIndex = i + 1; + break; + } + if (i === 0) { + lineStartIndex = 0; + break; + } + } + // Check if this asterisk is at the beginning of a line (with optional whitespace) + const beforeAsterisk = text.substring(lineStartIndex, index); + if (beforeAsterisk.trim() === "" && (nextChar === " " || nextChar === "\t")) { + // This is likely a list marker, don't count it + return acc; + } + if (prevChar !== "*" && nextChar !== "*") { + return acc + 1; + } + } + return acc; + }, 0); +}; + +// Completes incomplete italic formatting with single asterisks (*) +const handleIncompleteSingleAsteriskItalic = (text: string): string => { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) { + return text; + } + + const singleAsteriskMatch = text.match(singleAsteriskPattern); + + if (singleAsteriskMatch) { + // Find the first single asterisk position (not part of **) + let firstSingleAsteriskIndex = -1; + for (let i = 0; i < text.length; i++) { + if (text[i] === "*" && text[i - 1] !== "*" && text[i + 1] !== "*") { + firstSingleAsteriskIndex = i; + break; + } + } + + if (firstSingleAsteriskIndex === -1) { + return text; + } + + // Don't process if the marker is inside an incomplete code block + const openFenceIndex = getOpenCodeFenceIndex(text); + if (openFenceIndex !== -1 && firstSingleAsteriskIndex > openFenceIndex) { + return text; + } + + // Get content after the first single asterisk + const contentAfterFirstAsterisk = text.substring(firstSingleAsteriskIndex + 1); + + // Check if there's meaningful content after the asterisk + // Don't close if content is only whitespace or emphasis markers + if (!contentAfterFirstAsterisk || /^[\s_~*`]*$/.test(contentAfterFirstAsterisk)) { + return text; + } + + const singleAsterisks = countSingleAsterisks(text); + if (singleAsterisks % 2 === 1) { + return `${text}*`; + } + } + + return text; +}; + +// Check if a position is within a math block (between $ or $$) +const isWithinMathBlock = (text: string, position: number): boolean => { + // Count dollar signs before this position + let inInlineMath = false; + let inBlockMath = false; + + for (let i = 0; i < text.length && i < position; i++) { + // Skip escaped dollar signs + if (text[i] === "\\" && text[i + 1] === "$") { + i++; // Skip the next character + continue; + } + + if (text[i] === "$") { + // Check for block math ($$) + if (text[i + 1] === "$") { + inBlockMath = !inBlockMath; + i++; // Skip the second $ + inInlineMath = false; // Block math takes precedence + } else if (!inBlockMath) { + // Only toggle inline math if not in block math + inInlineMath = !inInlineMath; + } + } + } + + return inInlineMath || inBlockMath; +}; + +// Counts single underscores that are not part of double underscores, not escaped, and not in math blocks +const countSingleUnderscores = (text: string): number => { + return text.split("").reduce((acc, char, index) => { + if (char === "_") { + const prevChar = text[index - 1]; + const nextChar = text[index + 1]; + // Skip if escaped with backslash + if (prevChar === "\\") { + return acc; + } + // Skip if within math block + if (isWithinMathBlock(text, index)) { + return acc; + } + // Skip if underscore is word-internal (between word characters) + if ( + prevChar && + nextChar && + /[\p{L}\p{N}_]/u.test(prevChar) && + /[\p{L}\p{N}_]/u.test(nextChar) + ) { + return acc; + } + if (prevChar !== "_" && nextChar !== "_") { + return acc + 1; + } + } + return acc; + }, 0); +}; + +// Completes incomplete italic formatting with single underscores (_) +const handleIncompleteSingleUnderscoreItalic = (text: string): string => { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) { + return text; + } + + const singleUnderscoreMatch = text.match(singleUnderscorePattern); + + if (singleUnderscoreMatch) { + // Find the first single underscore position (not part of __ and not word-internal) + let firstSingleUnderscoreIndex = -1; + for (let i = 0; i < text.length; i++) { + if ( + text[i] === "_" && + text[i - 1] !== "_" && + text[i + 1] !== "_" && + text[i - 1] !== "\\" && + !isWithinMathBlock(text, i) + ) { + // Check if underscore is word-internal (between word characters) + const prevChar = i > 0 ? text[i - 1] : ""; + const nextChar = i < text.length - 1 ? text[i + 1] : ""; + if ( + prevChar && + nextChar && + /[\p{L}\p{N}_]/u.test(prevChar) && + /[\p{L}\p{N}_]/u.test(nextChar) + ) { + continue; + } + + firstSingleUnderscoreIndex = i; + break; + } + } + + if (firstSingleUnderscoreIndex === -1) { + return text; + } + + // Don't process if the marker is inside an incomplete code block + const openFenceIndex = getOpenCodeFenceIndex(text); + if (openFenceIndex !== -1 && firstSingleUnderscoreIndex > openFenceIndex) { + return text; + } + + // Get content after the first single underscore + const contentAfterFirstUnderscore = text.substring(firstSingleUnderscoreIndex + 1); + + // Check if there's meaningful content after the underscore + // Don't close if content is only whitespace or emphasis markers + if (!contentAfterFirstUnderscore || /^[\s_~*`]*$/.test(contentAfterFirstUnderscore)) { + return text; + } + + const singleUnderscores = countSingleUnderscores(text); + if (singleUnderscores % 2 === 1) { + // If text ends with newline(s), insert underscore before them + const trailingNewlineMatch = text.match(/\n+$/); + if (trailingNewlineMatch) { + const textBeforeNewlines = text.slice(0, -trailingNewlineMatch[0].length); + return `${textBeforeNewlines}_${trailingNewlineMatch[0]}`; + } + return `${text}_`; + } + } + + return text; +}; + +// Checks if a backtick at position i is part of a triple backtick sequence +const isPartOfTripleBacktick = (text: string, i: number): boolean => { + const isTripleStart = text.substring(i, i + 3) === "```"; + const isTripleMiddle = i > 0 && text.substring(i - 1, i + 2) === "```"; + const isTripleEnd = i > 1 && text.substring(i - 2, i + 1) === "```"; + + return isTripleStart || isTripleMiddle || isTripleEnd; +}; + +// Counts single backticks that are not part of triple backticks +const countSingleBackticks = (text: string): number => { + let count = 0; + for (let i = 0; i < text.length; i++) { + if (text[i] === "`" && !isPartOfTripleBacktick(text, i)) { + count++; + } + } + return count; +}; + +// Completes incomplete inline code formatting (`) +// Avoids completing if inside an incomplete code block +const handleIncompleteInlineCode = (text: string): string => { + // Check if we have inline triple backticks (starts with ``` and should end with ```) + // This pattern should ONLY match truly inline code (no newlines) + // Examples: ```code``` or ```python code``` + const inlineTripleBacktickMatch = text.match(/^```[^`\n]*```?$/); + if (inlineTripleBacktickMatch && !text.includes("\n")) { + // Check if it ends with exactly 2 backticks (incomplete) + if (text.endsWith("``") && !text.endsWith("```")) { + return `${text}\``; + } + // Already complete inline triple backticks + return text; + } + + // Check if we're inside a multi-line code block (complete or incomplete) + const allTripleBackticks = (text.match(/```/g) || []).length; + const insideIncompleteCodeBlock = allTripleBackticks % 2 === 1; + + // Don't modify text if we have complete multi-line code blocks (even pairs of ```) + if (allTripleBackticks > 0 && allTripleBackticks % 2 === 0 && text.includes("\n")) { + // We have complete multi-line code blocks, don't add any backticks + return text; + } + + // Special case: if text ends with ```\n (triple backticks followed by newline) + // This is actually a complete code block, not incomplete + if (text.endsWith("```\n") || text.endsWith("```")) { + // Count all triple backticks - if even, it's complete + if (allTripleBackticks % 2 === 0) { + return text; + } + } + + const inlineCodeMatch = text.match(inlineCodePattern); + + if (inlineCodeMatch && !insideIncompleteCodeBlock) { + // Don't close if there's no meaningful content after the opening marker + // inlineCodeMatch[2] contains the content after ` + // Check if content is only whitespace or other emphasis markers + const contentAfterMarker = inlineCodeMatch[2]; + if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) { + return text; + } + + const singleBacktickCount = countSingleBackticks(text); + if (singleBacktickCount % 2 === 1) { + return `${text}\``; + } + } + + return text; +}; + +// Completes incomplete strikethrough formatting (~~) +const handleIncompleteStrikethrough = (text: string): string => { + const strikethroughMatch = text.match(strikethroughPattern); + + if (strikethroughMatch) { + // Don't close if there's no meaningful content after the opening markers + // strikethroughMatch[2] contains the content after ~~ + // Check if content is only whitespace or other emphasis markers + const contentAfterMarker = strikethroughMatch[2]; + if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) { + return text; + } + + const tildePairs = (text.match(/~~/g) || []).length; + if (tildePairs % 2 === 1) { + return `${text}~~`; + } + } + + return text; +}; + +// Counts single dollar signs that are not part of double dollar signs and not escaped +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _countSingleDollarSigns = (text: string): number => { + return text.split("").reduce((acc, char, index) => { + if (char === "$") { + const prevChar = text[index - 1]; + const nextChar = text[index + 1]; + // Skip if escaped with backslash + if (prevChar === "\\") { + return acc; + } + if (prevChar !== "$" && nextChar !== "$") { + return acc + 1; + } + } + return acc; + }, 0); +}; + +// Completes incomplete block KaTeX formatting ($$) +const handleIncompleteBlockKatex = (text: string): string => { + // Count all $$ pairs in the text + const dollarPairs = (text.match(/\$\$/g) || []).length; + + // If we have an even number of $$, the block is complete + if (dollarPairs % 2 === 0) { + return text; + } + + // If we have an odd number, add closing $$ + // Check if this looks like a multi-line math block (contains newlines after opening $$) + const firstDollarIndex = text.indexOf("$$"); + const hasNewlineAfterStart = + firstDollarIndex !== -1 && text.indexOf("\n", firstDollarIndex) !== -1; + + // For multi-line blocks, add newline before closing $$ if not present + if (hasNewlineAfterStart && !text.endsWith("\n")) { + return `${text}\n$$`; + } + + // For inline blocks or when already ending with newline, just add $$ + return `${text}$$`; +}; + +// Counts triple asterisks that are not part of quadruple or more asterisks +const countTripleAsterisks = (text: string): number => { + let count = 0; + const matches = text.match(/\*+/g) || []; + + for (const match of matches) { + // Count how many complete triple asterisks are in this sequence + const asteriskCount = match.length; + if (asteriskCount >= 3) { + // Each group of exactly 3 asterisks counts as one triple asterisk marker + count += Math.floor(asteriskCount / 3); + } + } + + return count; +}; + +// Completes incomplete bold-italic formatting (***) +const handleIncompleteBoldItalic = (text: string): string => { + // Don't process if inside a complete code block + if (hasCompleteCodeBlock(text)) { + return text; + } + + // Don't process if text is only asterisks and has 4 or more consecutive asterisks + // This prevents cases like **** from being treated as incomplete *** + if (/^\*{4,}$/.test(text)) { + return text; + } + + const boldItalicMatch = text.match(boldItalicPattern); + + if (boldItalicMatch) { + // Don't close if there's no meaningful content after the opening markers + // boldItalicMatch[2] contains the content after *** + // Check if content is only whitespace or other emphasis markers + const contentAfterMarker = boldItalicMatch[2]; + if (!contentAfterMarker || /^[\s_~*`]*$/.test(contentAfterMarker)) { + return text; + } + + // Find the position of the matched bold-italic marker + const markerIndex = text.lastIndexOf(boldItalicMatch[1]); + + // Don't process if the marker is inside an incomplete code block + const openFenceIndex = getOpenCodeFenceIndex(text); + if (openFenceIndex !== -1 && markerIndex > openFenceIndex) { + return text; + } + + const tripleAsteriskCount = countTripleAsterisks(text); + if (tripleAsteriskCount % 2 === 1) { + return `${text}***`; + } + } + + return text; +}; + +// Parses markdown text and removes incomplete tokens to prevent partial rendering +export const parseIncompleteMarkdown = (text: string): string => { + if (!text || typeof text !== "string") { + return text; + } + + let result = text; + + // Handle incomplete links and images first + const processedResult = handleIncompleteLinksAndImages(result); + + // If we added an incomplete link marker, don't process other formatting + // as the content inside the link should be preserved as-is + if (processedResult.endsWith("](streamdown:incomplete-link)")) { + return processedResult; + } + + result = processedResult; + + // Handle various formatting completions + // Handle triple asterisks first (most specific) + result = handleIncompleteBoldItalic(result); + result = handleIncompleteBold(result); + result = handleIncompleteDoubleUnderscoreItalic(result); + result = handleIncompleteSingleAsteriskItalic(result); + result = handleIncompleteSingleUnderscoreItalic(result); + result = handleIncompleteInlineCode(result); + result = handleIncompleteStrikethrough(result); + + // Handle KaTeX formatting (only block math with $$) + result = handleIncompleteBlockKatex(result); + // Note: We don't handle inline KaTeX with single $ as they're likely currency symbols + + return result; +}; diff --git a/src/lib/utils/parseStringToList.ts b/src/lib/utils/parseStringToList.ts new file mode 100644 index 0000000000000000000000000000000000000000..a082057c665187d30fe0a49ceb84b9e7552ffc1e --- /dev/null +++ b/src/lib/utils/parseStringToList.ts @@ -0,0 +1,10 @@ +export function parseStringToList(links: unknown): string[] { + if (typeof links !== "string") { + throw new Error("Expected a string"); + } + + return links + .split(",") + .map((link) => link.trim()) + .filter((link) => link.length > 0); +} diff --git a/src/lib/utils/randomUuid.ts b/src/lib/utils/randomUuid.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d536365c57659305ad28d6fc06b89d77ab337ab --- /dev/null +++ b/src/lib/utils/randomUuid.ts @@ -0,0 +1,14 @@ +type UUID = ReturnType; + +export function randomUUID(): UUID { + // Only on old safari / ios + if (!("randomUUID" in crypto)) { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => + ( + Number(c) ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (Number(c) / 4))) + ).toString(16) + ) as UUID; + } + return crypto.randomUUID(); +} diff --git a/src/lib/utils/searchTokens.ts b/src/lib/utils/searchTokens.ts new file mode 100644 index 0000000000000000000000000000000000000000..012df9b644736de94c2c7cfa12f70f5d5eb4b253 --- /dev/null +++ b/src/lib/utils/searchTokens.ts @@ -0,0 +1,33 @@ +const PUNCTUATION_REGEX = /\p{P}/gu; + +function removeDiacritics(s: string, form: "NFD" | "NFKD" = "NFD"): string { + return s.normalize(form).replace(/[\u0300-\u036f]/g, ""); +} + +export function generateSearchTokens(value: string): string[] { + const fullTitleToken = removeDiacritics(value) + .replace(PUNCTUATION_REGEX, "") + .replaceAll(/\s+/g, "") + .toLowerCase(); + return [ + ...new Set([ + ...removeDiacritics(value) + .split(/\s+/) + .map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase()) + .filter((word) => word.length), + ...(fullTitleToken.length ? [fullTitleToken] : []), + ]), + ]; +} + +function escapeForRegExp(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +} + +export function generateQueryTokens(query: string): RegExp[] { + return removeDiacritics(query) + .split(/\s+/) + .map((word) => word.replace(PUNCTUATION_REGEX, "").toLowerCase()) + .filter((word) => word.length) + .map((token) => new RegExp(`^${escapeForRegExp(token)}`)); +} diff --git a/src/lib/utils/sha256.ts b/src/lib/utils/sha256.ts new file mode 100644 index 0000000000000000000000000000000000000000..43059b518fc5a4da6ed08ab36aeb6c289007f6aa --- /dev/null +++ b/src/lib/utils/sha256.ts @@ -0,0 +1,7 @@ +export async function sha256(input: string): Promise { + const utf8 = new TextEncoder().encode(input); + const hashBuffer = await crypto.subtle.digest("SHA-256", utf8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((bytes) => bytes.toString(16).padStart(2, "0")).join(""); + return hashHex; +} diff --git a/src/lib/utils/stringifyError.ts b/src/lib/utils/stringifyError.ts new file mode 100644 index 0000000000000000000000000000000000000000..a182d0974e0f8570d392d94fb957fcf49bc17692 --- /dev/null +++ b/src/lib/utils/stringifyError.ts @@ -0,0 +1,12 @@ +/** Takes an unknown error and attempts to convert it to a string */ +export function stringifyError(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === "string") return error; + if (typeof error === "object" && error !== null) { + // try a few common properties + if ("message" in error && typeof error.message === "string") return error.message; + if ("body" in error && typeof error.body === "string") return error.body; + if ("name" in error && typeof error.name === "string") return error.name; + } + return "Unknown error"; +} diff --git a/src/lib/utils/sum.ts b/src/lib/utils/sum.ts new file mode 100644 index 0000000000000000000000000000000000000000..289b70584ef9f7795b1f4b1bf0151237dc2c55ff --- /dev/null +++ b/src/lib/utils/sum.ts @@ -0,0 +1,3 @@ +export function sum(nums: number[]): number { + return nums.reduce((a, b) => a + b, 0); +} diff --git a/src/lib/utils/template.spec.ts b/src/lib/utils/template.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f3462b6ef006d42e4c17b40c995f92422c0762d --- /dev/null +++ b/src/lib/utils/template.spec.ts @@ -0,0 +1,59 @@ +import { describe, test, expect } from "vitest"; +import { compileTemplate } from "./template"; + +// Test data for simple templates +const modelData = { + preprompt: "Hello", +}; + +const simpleTemplate = "Test: {{preprompt}} and {{foo}}"; + +// Additional realistic test data for Llama 70B templates +const messages = [ + { from: "user", content: "Hello there" }, + { from: "assistant", content: "Hi, how can I help?" }, +]; + +// Handlebars Llama 70B Template +const llama70bTemplateHB = `{{#if preprompt}}Source: system\n\n{{preprompt}}{{/if}}{{#each messages}}{{#ifUser}}Source: user\n\n{{content}}{{/ifUser}}{{#ifAssistant}}Source: assistant\n\n{{content}}{{/ifAssistant}}{{/each}}Source: assistant\nDestination: user\n\n`; + +// Expected output for Handlebars Llama 70B Template +const expectedHB = + "Source: system\n\nSystem MessageSource: user\n\nHello thereSource: assistant\n\nHi, how can I help?Source: assistant\nDestination: user\n\n"; + +// Jinja Llama 70B Template +const llama70bTemplateJinja = `{% if preprompt %}Source: system\n\n{{ preprompt }}{% endif %}{% for message in messages %}{% if message.from == 'user' %}Source: user\n\n{{ message.content }}{% elif message.from == 'assistant' %}Source: assistant\n\n{{ message.content }}{% endif %}{% endfor %}Source: assistant\nDestination: user\n\n`; + +// Expected output for Jinja Llama 70B Template +const expectedJinja = + "Source: system\n\nSystem MessageSource: user\n\nHello thereSource: assistant\n\nHi, how can I help?Source: assistant\nDestination: user\n\n"; + +describe("Template Engine Rendering", () => { + test("should render using Handlebars fallback when no templateEngine is specified", () => { + const render = compileTemplate(simpleTemplate, modelData); + const result = render({ foo: "World" }); + expect(result).toBe("Test: Hello and World"); + }); + + test('should render using Jinja when templateEngine is set to "jinja"', () => { + const render = compileTemplate(simpleTemplate, modelData); + const result = render({ foo: "World" }); + expect(result).toBe("Test: Hello and World"); + }); + + // Realistic Llama 70B template tests + test("should render realistic Llama 70B template using Handlebars", () => { + const render = compileTemplate(llama70bTemplateHB, { preprompt: "System Message" }); + const result = render({ messages }); + expect(result).toBe(expectedHB); + }); + + test("should render realistic Llama 70B template using Jinja", () => { + const render = compileTemplate(llama70bTemplateJinja, { + preprompt: "System Message", + }); + const result = render({ messages }); + // Trim both outputs to account for whitespace differences in Jinja engine + expect(result.trim()).toBe(expectedJinja.trim()); + }); +}); diff --git a/src/lib/utils/template.ts b/src/lib/utils/template.ts new file mode 100644 index 0000000000000000000000000000000000000000..275c1aeccecc0a4dcf7564d5e101f24a0fe2a3ef --- /dev/null +++ b/src/lib/utils/template.ts @@ -0,0 +1,53 @@ +import type { Message } from "$lib/types/Message"; +import Handlebars from "handlebars"; +import { Template } from "@huggingface/jinja"; +import { logger } from "$lib/server/logger"; + +// Register Handlebars helpers +Handlebars.registerHelper("ifUser", function (this: Pick, options) { + if (this.from == "user") return options.fn(this); +}); + +Handlebars.registerHelper( + "ifAssistant", + function (this: Pick, options) { + if (this.from == "assistant") return options.fn(this); + } +); + +// Updated compileTemplate to try Jinja and fallback to Handlebars if Jinja fails +export function compileTemplate( + input: string, + model: { preprompt: string; templateEngine?: string } +) { + let jinjaTemplate: Template | undefined; + try { + // Try to compile with Jinja + jinjaTemplate = new Template(input); + } catch (e) { + // logger.error(e, "Could not compile with Jinja"); + // Could not compile with Jinja + jinjaTemplate = undefined; + } + + const hbTemplate = Handlebars.compile(input, { + knownHelpers: { ifUser: true, ifAssistant: true }, + knownHelpersOnly: true, + noEscape: true, + strict: true, + preventIndent: true, + }); + + return function render(inputs: T) { + if (jinjaTemplate) { + try { + return jinjaTemplate.render({ ...model, ...inputs }); + } catch (e) { + logger.error(e, "Could not render with Jinja"); + // Fallback to Handlebars if Jinja rendering fails + return hbTemplate({ ...model, ...inputs }); + } + } + return hbTemplate({ ...model, ...inputs }); + }; +} diff --git a/src/lib/utils/timeout.ts b/src/lib/utils/timeout.ts new file mode 100644 index 0000000000000000000000000000000000000000..355edd12e5f2257a7b30309b1fca8fc8cb592c8d --- /dev/null +++ b/src/lib/utils/timeout.ts @@ -0,0 +1,9 @@ +export const timeout = (prom: Promise, time: number): Promise => { + let timer: NodeJS.Timeout; + return Promise.race([ + prom, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timeout after ${time / 1000} seconds`)), time); + }), + ]).finally(() => clearTimeout(timer)); +}; diff --git a/src/lib/utils/tree/addChildren.spec.ts b/src/lib/utils/tree/addChildren.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c7861f2cbeb6904c2d784ae298a1fd9992a897c --- /dev/null +++ b/src/lib/utils/tree/addChildren.spec.ts @@ -0,0 +1,102 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec"; +import { addChildren } from "./addChildren"; +import type { Message } from "$lib/types/Message"; + +const newMessage: Omit = { + content: "new message", + from: "user", +}; + +Object.freeze(newMessage); + +describe("addChildren", async () => { + it("should let you append on legacy conversations", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const convLength = conv.messages.length; + + addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id); + expect(conv.messages.length).toEqual(convLength + 1); + }); + it("should not let you create branches on legacy conversations", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addChildren(conv, newMessage, conv.messages[0].id)).toThrow(); + }); + it("should not let you create a message that already exists", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const messageThatAlreadyExists: Message = { + id: conv.messages[0].id, + content: "new message", + from: "user", + }; + + expect(() => addChildren(conv, messageThatAlreadyExists, conv.messages[0].id)).toThrow(); + }); + it("should let you create branches on conversations with subtrees", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const nChildren = conv.messages[0].children?.length; + if (!nChildren) throw new Error("No children found"); + addChildren(conv, newMessage, conv.messages[0].id); + expect(conv.messages[0].children?.length).toEqual(nChildren + 1); + }); + + it("should let you create a new leaf", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const parentId = conv.messages[conv.messages.length - 1].id; + const nChildren = conv.messages[conv.messages.length - 1].children?.length; + + if (nChildren === undefined) throw new Error("No children found"); + expect(nChildren).toEqual(0); + + addChildren(conv, newMessage, parentId); + expect(conv.messages[conv.messages.length - 2].children?.length).toEqual(nChildren + 1); + }); + + it("should let you append to an empty conversation without specifying a parentId", async () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [] as Message[], + }; + + addChildren(conv, newMessage); + expect(conv.messages.length).toEqual(1); + expect(conv.rootMessageId).toEqual(conv.messages[0].id); + }); + + it("should throw if you don't specify a parentId in a conversation with messages", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addChildren(conv, newMessage)).toThrow(); + }); + + it("should return the id of the new message", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(addChildren(conv, newMessage, conv.messages[conv.messages.length - 1].id)).toEqual( + conv.messages[conv.messages.length - 1].id + ); + }); +}); diff --git a/src/lib/utils/tree/addChildren.ts b/src/lib/utils/tree/addChildren.ts new file mode 100644 index 0000000000000000000000000000000000000000..82b160409bb2ebb91ceb71d87a45f975f81a9039 --- /dev/null +++ b/src/lib/utils/tree/addChildren.ts @@ -0,0 +1,48 @@ +import { v4 } from "uuid"; +import type { Tree, TreeId, NewNode, TreeNode } from "./tree"; + +export function addChildren(conv: Tree, message: NewNode, parentId?: TreeId): TreeId { + // if this is the first message we just push it + if (conv.messages.length === 0) { + const messageId = v4(); + conv.rootMessageId = messageId; + conv.messages.push({ + ...message, + ancestors: [], + id: messageId, + } as TreeNode); + return messageId; + } + + if (!parentId) { + throw new Error("You need to specify a parentId if this is not the first message"); + } + + const messageId = v4(); + if (!conv.rootMessageId) { + // if there is no parentId we just push the message + if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) { + throw new Error("This is a legacy conversation, you can only append to the last message"); + } + conv.messages.push({ ...message, id: messageId } as TreeNode); + return messageId; + } + + const ancestors = [...(conv.messages.find((m) => m.id === parentId)?.ancestors ?? []), parentId]; + conv.messages.push({ + ...message, + ancestors, + id: messageId, + children: [], + } as TreeNode); + + const parent = conv.messages.find((m) => m.id === parentId); + + if (parent) { + if (parent.children) { + parent.children.push(messageId); + } else parent.children = [messageId]; + } + + return messageId; +} diff --git a/src/lib/utils/tree/addSibling.spec.ts b/src/lib/utils/tree/addSibling.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3865aeacc0bb8858dd45a9942e24c65a90d7f24 --- /dev/null +++ b/src/lib/utils/tree/addSibling.spec.ts @@ -0,0 +1,81 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec"; +import type { Message } from "$lib/types/Message"; +import { addSibling } from "./addSibling"; +import type { Conversation } from "$lib/types/Conversation"; + +const newMessage = { + content: "new message", + from: "user" as const, +}; + +Object.freeze(newMessage); + +describe("addSibling", async () => { + it("should fail on empty conversations", () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [] as Message[], + } satisfies Pick; + + expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow( + "Cannot add a sibling to an empty conversation" + ); + }); + + it("should fail on legacy conversations", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addSibling(conv, newMessage, conv.messages[0].id)).toThrow( + "Cannot add a sibling to a legacy conversation" + ); + }); + + it("should fail if the sibling message doesn't exist", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow( + "The sibling message doesn't exist" + ); + }); + + // TODO: This behaviour should be fixed, we do not need to fail on the root message. + it("should fail if the sibling message is the root message", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + if (!conv.rootMessageId) throw new Error("Root message not found"); + + expect(() => addSibling(conv, newMessage, conv.rootMessageId as Message["id"])).toThrow( + "The sibling message is the root message, therefore we can't add a sibling" + ); + }); + + it("should add a sibling to a message", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + // add sibling and check children count for parnets + + const nChildren = conv.messages[1].children?.length; + const siblingId = addSibling(conv, newMessage, conv.messages[2].id); + const nChildrenNew = conv.messages[1].children?.length; + + if (!nChildren) throw new Error("No children found"); + + expect(nChildrenNew).toBe(nChildren + 1); + + // make sure siblings have the same ancestors + const sibling = conv.messages.find((m) => m.id === siblingId); + expect(sibling?.ancestors).toEqual(conv.messages[2].ancestors); + }); +}); diff --git a/src/lib/utils/tree/addSibling.ts b/src/lib/utils/tree/addSibling.ts new file mode 100644 index 0000000000000000000000000000000000000000..42658b2a0abeb04647bc958d8ccde67c1060e63a --- /dev/null +++ b/src/lib/utils/tree/addSibling.ts @@ -0,0 +1,41 @@ +import { v4 } from "uuid"; +import type { Tree, TreeId, NewNode, TreeNode } from "./tree"; + +export function addSibling(conv: Tree, message: NewNode, siblingId: TreeId): TreeId { + if (conv.messages.length === 0) { + throw new Error("Cannot add a sibling to an empty conversation"); + } + if (!conv.rootMessageId) { + throw new Error("Cannot add a sibling to a legacy conversation"); + } + + const sibling = conv.messages.find((m) => m.id === siblingId); + + if (!sibling) { + throw new Error("The sibling message doesn't exist"); + } + + if (!sibling.ancestors || sibling.ancestors?.length === 0) { + throw new Error("The sibling message is the root message, therefore we can't add a sibling"); + } + + const messageId = v4(); + + conv.messages.push({ + ...message, + id: messageId, + ancestors: sibling.ancestors, + children: [], + } as TreeNode); + + const nearestAncestorId = sibling.ancestors[sibling.ancestors.length - 1]; + const nearestAncestor = conv.messages.find((m) => m.id === nearestAncestorId); + + if (nearestAncestor) { + if (nearestAncestor.children) { + nearestAncestor.children.push(messageId); + } else nearestAncestor.children = [messageId]; + } + + return messageId; +} diff --git a/src/lib/utils/tree/buildSubtree.spec.ts b/src/lib/utils/tree/buildSubtree.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..936fb8a2023417af71dca979154192ff9eb0ce73 --- /dev/null +++ b/src/lib/utils/tree/buildSubtree.spec.ts @@ -0,0 +1,110 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { + insertLegacyConversation, + insertLinearBranchConversation, + insertSideBranchesConversation, +} from "./treeHelpers.spec"; +import { buildSubtree } from "./buildSubtree"; + +describe("buildSubtree", () => { + it("a subtree in a legacy conversation should be just a slice", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + // check middle + const id = conv.messages[2].id; + const subtree = buildSubtree(conv, id); + expect(subtree).toEqual(conv.messages.slice(0, 3)); + + // check zero + const id2 = conv.messages[0].id; + const subtree2 = buildSubtree(conv, id2); + expect(subtree2).toEqual(conv.messages.slice(0, 1)); + + //check full length + const id3 = conv.messages[conv.messages.length - 1].id; + const subtree3 = buildSubtree(conv, id3); + expect(subtree3).toEqual(conv.messages); + }); + + it("a subtree in a linear branch conversation should be the ancestors and the message", async () => { + const convId = await insertLinearBranchConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + // check middle + const id = conv.messages[1].id; + const subtree = buildSubtree(conv, id); + expect(subtree).toEqual([conv.messages[0], conv.messages[1]]); + + // check zero + const id2 = conv.messages[0].id; + const subtree2 = buildSubtree(conv, id2); + expect(subtree2).toEqual([conv.messages[0]]); + + //check full length + const id3 = conv.messages[conv.messages.length - 1].id; + const subtree3 = buildSubtree(conv, id3); + expect(subtree3).toEqual(conv.messages); + }); + + it("should throw an error if the message is not found", async () => { + const convId = await insertLinearBranchConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const id = "not-a-real-id-test"; + + expect(() => buildSubtree(conv, id)).toThrow("Message not found"); + }); + + it("should throw an error if the ancestor is not found", async () => { + const convId = await insertLinearBranchConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const id = "1-1-1-1-2"; + + conv.messages[1].ancestors = ["not-a-real-id-test"]; + + expect(() => buildSubtree(conv, id)).toThrow("Ancestor not found"); + }); + + it("should work on empty conversations", () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [], + }; + + const subtree = buildSubtree(conv, "not-a-real-id-test"); + expect(subtree).toEqual([]); + }); + + it("should work for conversation with subtrees", async () => { + const convId = await insertSideBranchesConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const subtree = buildSubtree(conv, "1-1-1-1-2"); + expect(subtree).toEqual([conv.messages[0], conv.messages[1]]); + + const subtree2 = buildSubtree(conv, "1-1-1-1-4"); + expect(subtree2).toEqual([ + conv.messages[0], + conv.messages[1], + conv.messages[2], + conv.messages[3], + ]); + + const subtree3 = buildSubtree(conv, "1-1-1-1-6"); + expect(subtree3).toEqual([conv.messages[0], conv.messages[4], conv.messages[5]]); + + const subtree4 = buildSubtree(conv, "1-1-1-1-7"); + expect(subtree4).toEqual([conv.messages[0], conv.messages[4], conv.messages[6]]); + }); +}); diff --git a/src/lib/utils/tree/buildSubtree.ts b/src/lib/utils/tree/buildSubtree.ts new file mode 100644 index 0000000000000000000000000000000000000000..68d346551eeba461ef6e6cba63ee2df1254eae89 --- /dev/null +++ b/src/lib/utils/tree/buildSubtree.ts @@ -0,0 +1,24 @@ +import type { Tree, TreeId, TreeNode } from "./tree"; + +export function buildSubtree(conv: Tree, id: TreeId): TreeNode[] { + if (!conv.rootMessageId) { + if (conv.messages.length === 0) return []; + // legacy conversation slice up to id + const index = conv.messages.findIndex((m) => m.id === id); + if (index === -1) throw new Error("Message not found"); + return conv.messages.slice(0, index + 1); + } else { + // find the message with the right id then create the ancestor tree + const message = conv.messages.find((m) => m.id === id); + if (!message) throw new Error("Message not found"); + + return [ + ...(message.ancestors?.map((ancestorId) => { + const ancestor = conv.messages.find((m) => m.id === ancestorId); + if (!ancestor) throw new Error("Ancestor not found"); + return ancestor; + }) ?? []), + message, + ]; + } +} diff --git a/src/lib/utils/tree/convertLegacyConversation.spec.ts b/src/lib/utils/tree/convertLegacyConversation.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8adc55abac3c7bc084fa7935fd94a4be5c776a0 --- /dev/null +++ b/src/lib/utils/tree/convertLegacyConversation.spec.ts @@ -0,0 +1,31 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +import { convertLegacyConversation } from "./convertLegacyConversation"; +import { insertLegacyConversation } from "./treeHelpers.spec"; + +describe("convertLegacyConversation", () => { + it("should convert a legacy conversation", async () => { + const convId = await insertLegacyConversation(); + const conv = await collections.conversations.findOne({ _id: new ObjectId(convId) }); + if (!conv) throw new Error("Conversation not found"); + + const newConv = convertLegacyConversation(conv); + + expect(newConv.rootMessageId).toBe(newConv.messages[0].id); + expect(newConv.messages[0].ancestors).toEqual([]); + expect(newConv.messages[1].ancestors).toEqual([newConv.messages[0].id]); + expect(newConv.messages[0].children).toEqual([newConv.messages[1].id]); + }); + it("should work on empty conversations", async () => { + const conv = { + _id: new ObjectId(), + rootMessageId: undefined, + messages: [], + }; + const newConv = convertLegacyConversation(conv); + expect(newConv.rootMessageId).toBe(undefined); + expect(newConv.messages).toEqual([]); + }); +}); diff --git a/src/lib/utils/tree/convertLegacyConversation.ts b/src/lib/utils/tree/convertLegacyConversation.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b14468a6296203b53c56889b93d892afa1ffd37 --- /dev/null +++ b/src/lib/utils/tree/convertLegacyConversation.ts @@ -0,0 +1,36 @@ +import type { Conversation } from "$lib/types/Conversation"; +import type { Message } from "$lib/types/Message"; +import { v4 } from "uuid"; + +export function convertLegacyConversation( + conv: Pick +): Pick { + if (conv.rootMessageId) return conv; // not a legacy conversation + if (conv.messages.length === 0) return conv; // empty conversation + const messages = [ + { + from: "system", + content: conv.preprompt ?? "", + createdAt: new Date(), + updatedAt: new Date(), + id: v4(), + } satisfies Message, + ...conv.messages, + ]; + + const rootMessageId = messages[0].id; + + const newMessages = messages.map((message, index) => { + return { + ...message, + ancestors: messages.slice(0, index).map((m) => m.id), + children: index < messages.length - 1 ? [messages[index + 1].id] : [], + }; + }); + + return { + ...conv, + rootMessageId, + messages: newMessages, + }; +} diff --git a/src/lib/utils/tree/isMessageId.spec.ts b/src/lib/utils/tree/isMessageId.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..91b2baef576d9a73e1effe4e8c7df8b3f67d1e1d --- /dev/null +++ b/src/lib/utils/tree/isMessageId.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { isMessageId } from "./isMessageId"; +import { v4 } from "uuid"; + +describe("isMessageId", () => { + it("should return true for a valid message id", () => { + expect(isMessageId(v4())).toBe(true); + }); + it("should return false for an invalid message id", () => { + expect(isMessageId("1-2-3-4")).toBe(false); + }); + it("should return false for an empty string", () => { + expect(isMessageId("")).toBe(false); + }); +}); diff --git a/src/lib/utils/tree/isMessageId.ts b/src/lib/utils/tree/isMessageId.ts new file mode 100644 index 0000000000000000000000000000000000000000..e46b4526cac3dcb037471fbc77974e33ee2021a6 --- /dev/null +++ b/src/lib/utils/tree/isMessageId.ts @@ -0,0 +1,5 @@ +import type { Message } from "$lib/types/Message"; + +export function isMessageId(id: string): id is Message["id"] { + return id.split("-").length === 5; +} diff --git a/src/lib/utils/tree/tree.d.ts b/src/lib/utils/tree/tree.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..9bbb6a10f896ac5a63ab2996906a41ef50549f24 --- /dev/null +++ b/src/lib/utils/tree/tree.d.ts @@ -0,0 +1,14 @@ +export type TreeId = string; + +export type Tree = { + rootMessageId?: TreeId; + messages: TreeNode[]; +}; + +export type TreeNode = T & { + id: TreeId; + ancestors?: TreeId[]; + children?: TreeId[]; +}; + +export type NewNode = Omit, "id">; diff --git a/src/lib/utils/tree/treeHelpers.spec.ts b/src/lib/utils/tree/treeHelpers.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fce45bc0e30bb1b9622dbdf33e2ed0f3320d286 --- /dev/null +++ b/src/lib/utils/tree/treeHelpers.spec.ts @@ -0,0 +1,163 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; + +// function used to insert conversations used for testing + +export const insertLegacyConversation = async () => { + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + title: "legacy conversation", + model: "", + + messages: [ + { + id: "1-1-1-1-1", + from: "user", + content: "Hello, world! I am a user", + }, + { + id: "1-1-1-1-2", + from: "assistant", + content: "Hello, world! I am an assistant.", + }, + { + id: "1-1-1-1-3", + from: "user", + content: "Hello, world! I am a user.", + }, + { + id: "1-1-1-1-4", + from: "assistant", + content: "Hello, world! I am an assistant.", + }, + ], + }); + return res.insertedId; +}; + +export const insertLinearBranchConversation = async () => { + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + title: "linear branch conversation", + model: "", + + rootMessageId: "1-1-1-1-1", + messages: [ + { + id: "1-1-1-1-1", + from: "user", + content: "Hello, world! I am a user", + ancestors: [], + children: ["1-1-1-1-2"], + }, + { + id: "1-1-1-1-2", + from: "assistant", + content: "Hello, world! I am an assistant.", + ancestors: ["1-1-1-1-1"], + children: ["1-1-1-1-3"], + }, + { + id: "1-1-1-1-3", + from: "user", + content: "Hello, world! I am a user.", + ancestors: ["1-1-1-1-1", "1-1-1-1-2"], + children: ["1-1-1-1-4"], + }, + { + id: "1-1-1-1-4", + from: "assistant", + content: "Hello, world! I am an assistant.", + ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"], + children: [], + }, + ], + }); + return res.insertedId; +}; + +export const insertSideBranchesConversation = async () => { + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + title: "side branches conversation", + model: "", + + rootMessageId: "1-1-1-1-1", + messages: [ + { + id: "1-1-1-1-1", + from: "user", + content: "Hello, world, root message!", + ancestors: [], + children: ["1-1-1-1-2", "1-1-1-1-5"], + }, + { + id: "1-1-1-1-2", + from: "assistant", + content: "Hello, response to root message!", + ancestors: ["1-1-1-1-1"], + children: ["1-1-1-1-3"], + }, + { + id: "1-1-1-1-3", + from: "user", + content: "Hello, follow up question!", + ancestors: ["1-1-1-1-1", "1-1-1-1-2"], + children: ["1-1-1-1-4"], + }, + { + id: "1-1-1-1-4", + from: "assistant", + content: "Hello, response from follow up question!", + ancestors: ["1-1-1-1-1", "1-1-1-1-2", "1-1-1-1-3"], + children: [], + }, + { + id: "1-1-1-1-5", + from: "assistant", + content: "Hello, alternative assistant answer!", + ancestors: ["1-1-1-1-1"], + children: ["1-1-1-1-6", "1-1-1-1-7"], + }, + { + id: "1-1-1-1-6", + from: "user", + content: "Hello, follow up question to alternative answer!", + ancestors: ["1-1-1-1-1", "1-1-1-1-5"], + children: [], + }, + { + id: "1-1-1-1-7", + from: "user", + content: "Hello, alternative follow up question to alternative answer!", + ancestors: ["1-1-1-1-1", "1-1-1-1-5"], + children: [], + }, + ], + }); + return res.insertedId; +}; + +describe("inserting conversations", () => { + it("should insert a legacy conversation", async () => { + const id = await insertLegacyConversation(); + expect(id).toBeDefined(); + }); + + it("should insert a linear branch conversation", async () => { + const id = await insertLinearBranchConversation(); + expect(id).toBeDefined(); + }); + + it("should insert a side branches conversation", async () => { + const id = await insertSideBranchesConversation(); + expect(id).toBeDefined(); + }); +}); diff --git a/src/lib/utils/updates.ts b/src/lib/utils/updates.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c8f1f81a09491789775a34912049201d3c447a7 --- /dev/null +++ b/src/lib/utils/updates.ts @@ -0,0 +1,39 @@ +// This is a debouncer for the updates from the server to the client +// It is used to prevent the client from being overloaded with too many updates +// It works by keeping track of the time it takes to render the updates +// and adding a safety margin to it, to find the debounce time. + +class UpdateDebouncer { + private renderStartedAt: Date | null = null; + private lastRenderTimes: number[] = []; + + get maxUpdateTime() { + if (this.lastRenderTimes.length === 0) { + return 50; + } + + const averageTime = + this.lastRenderTimes.reduce((acc, time) => acc + time, 0) / this.lastRenderTimes.length; + + return Math.min(averageTime * 3, 500); + } + + public startRender() { + this.renderStartedAt = new Date(); + } + + public endRender() { + if (!this.renderStartedAt) { + return; + } + + const timeSinceRenderStarted = new Date().getTime() - this.renderStartedAt.getTime(); + this.lastRenderTimes.push(timeSinceRenderStarted); + if (this.lastRenderTimes.length > 10) { + this.lastRenderTimes.shift(); + } + this.renderStartedAt = null; + } +} + +export const updateDebouncer = new UpdateDebouncer(); diff --git a/src/lib/utils/urlParams.ts b/src/lib/utils/urlParams.ts new file mode 100644 index 0000000000000000000000000000000000000000..c85699a939bff3f8f8d0603bec2eeea0d0d5c765 --- /dev/null +++ b/src/lib/utils/urlParams.ts @@ -0,0 +1,13 @@ +const MAX_PARAM_LENGTH = 10_000; + +export function sanitizeUrlParam(value: string | null): string | null { + if (value == null) return null; + + const trimmed = value.trim(); + if (!trimmed.length) return null; + if (trimmed.length > MAX_PARAM_LENGTH) return null; + + return trimmed; +} + +export { MAX_PARAM_LENGTH }; diff --git a/src/lib/workers/markdownWorker.ts b/src/lib/workers/markdownWorker.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7fa57d8b6cb446f0d11d3bdbc903c0b483bb6de --- /dev/null +++ b/src/lib/workers/markdownWorker.ts @@ -0,0 +1,57 @@ +// Simple type to replace removed WebSearchSource +type SimpleSource = { + title?: string; + link: string; +}; +import { processTokens, type Token } from "$lib/utils/marked"; + +export type IncomingMessage = { + type: "process"; + content: string; + sources: SimpleSource[]; +}; + +export type OutgoingMessage = { + type: "processed"; + tokens: Token[]; +}; + +// Flag to track if the worker is currently processing a message +let isProcessing = false; + +// Buffer to store the latest incoming message +let latestMessage: IncomingMessage | null = null; + +// Helper function to safely handle the latest message +async function processMessage() { + if (latestMessage) { + const nextMessage = latestMessage; + + latestMessage = null; + isProcessing = true; + + try { + const { content, sources } = nextMessage; + const processedTokens = await processTokens(content, sources); + postMessage(JSON.parse(JSON.stringify({ type: "processed", tokens: processedTokens }))); + } finally { + isProcessing = false; + + // After processing, check if a new message was buffered + await new Promise((resolve) => setTimeout(resolve, 100)); + processMessage(); + } + } +} + +onmessage = (event) => { + if (event.data.type !== "process") { + return; + } + + latestMessage = event.data as IncomingMessage; + + if (!isProcessing && latestMessage) { + processMessage(); + } +}; diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c0eddca222422904bed308fe355c03d1cf06da91 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,20 @@ + + +
+
+

{page.status}

+
+

{page.error?.message}

+ {#if page.error?.errorId} +
+
{page.error
+					.errorId}
+ {/if} +
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1ec9039c5efba307c8489d15655c80d7d848a332 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,298 @@ + + + + {publicConfig.PUBLIC_APP_NAME} + + + + + + + {#if !page.url.pathname.includes("/models/")} + + + + + + {/if} + + {#if publicConfig.PUBLIC_ORIGIN} + + + {:else} + + {/if} + + + + {#if publicConfig.PUBLIC_PLAUSIBLE_SCRIPT_URL} + + {/if} + + {#if publicConfig.PUBLIC_APPLE_APP_ID} + + {/if} + + +{#if showWelcome} + +{/if} + + + +
+ (isNavCollapsed = !isNavCollapsed)} + classNames="absolute inset-y-0 z-10 my-auto {!isNavCollapsed + ? 'left-[290px]' + : 'left-0'} *:transition-transform" + /> + + {#if canShare} + + {/if} + + + deleteConversation(id)} + oneditConversationTitle={(payload) => editConversationTitle(payload.id, payload.title)} + /> + + + {#if currentError} + + {/if} + {@render children?.()} + + {#if publicConfig.PUBLIC_PLAUSIBLE_SCRIPT_URL} + + {/if} +
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..106f8d9883fa1b906b38e6c50f4bd08147e2c21f --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,53 @@ +import { UrlDependency } from "$lib/types/UrlDependency"; +import type { ConvSidebar } from "$lib/types/ConvSidebar"; +import { useAPIClient, handleResponse } from "$lib/APIClient"; +import { getConfigManager } from "$lib/utils/PublicConfig.svelte"; + +export const load = async ({ depends, fetch, url }) => { + depends(UrlDependency.ConversationList); + + const client = useAPIClient({ fetch, origin: url.origin }); + + const [settings, models, user, publicConfig, featureFlags, conversationsData] = await Promise.all( + [ + client.user.settings.get().then(handleResponse), + client.models.get().then(handleResponse), + client.user.get().then(handleResponse), + client["public-config"].get().then(handleResponse), + client["feature-flags"].get().then(handleResponse), + client.conversations.get({ query: { p: 0 } }).then(handleResponse), + ] + ); + + const defaultModel = models[0]; + + const { conversations: rawConversations, nConversations } = conversationsData; + const conversations = rawConversations.map((conv) => { + const trimmedTitle = conv.title.trim(); + + conv.title = trimmedTitle; + + return { + id: conv._id.toString(), + title: conv.title, + model: conv.model ?? defaultModel, + updatedAt: new Date(conv.updatedAt), + } satisfies ConvSidebar; + }); + + return { + nConversations, + conversations, + models, + oldModels: [], + user, + settings: { + ...settings, + welcomeModalSeenAt: settings.welcomeModalSeenAt + ? new Date(settings.welcomeModalSeenAt) + : null, + }, + publicConfig: getConfigManager(publicConfig), + ...featureFlags, + }; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5a8aa50f1324983103ffe29afb72bde78f5eb0e3 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,149 @@ + + + + {publicConfig.PUBLIC_APP_NAME} + + +{#if hasModels} + createConversation(message)} + loading={$loading} + {currentModel} + models={data.models} + bind:files + bind:draft + /> +{:else} +
+

No models available

+

+ No chat models are configured. Set `OPENAI_BASE_URL` and ensure the server can reach the + endpoint, then reload. If unset, the app defaults to the Hugging Face router. +

+
+{/if} diff --git a/src/routes/.well-known/.oauth-cimd/+server.ts b/src/routes/.well-known/.oauth-cimd/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..41c35c79b9bc532c707f4136a5a17a900246eb98 --- /dev/null +++ b/src/routes/.well-known/.oauth-cimd/+server.ts @@ -0,0 +1,27 @@ +import { OIDConfig } from "$lib/server/auth"; +import { config } from "$lib/server/config"; + +export const GET = ({ url }) => { + if (!OIDConfig.CLIENT_ID) { + return new Response("Client ID not found", { status: 404 }); + } + if (OIDConfig.CLIENT_ID !== "__CIMD__") { + return new Response("Client ID is manually set to something other than '__CIMD__'", { + status: 404, + }); + } + return new Response( + JSON.stringify({ + client_id: new URL("/.well-known/oauth-cimd", config.PUBLIC_ORIGIN || url.origin).toString(), + client_name: config.PUBLIC_APP_NAME, + redirect_uris: [new URL("/login/callback", config.PUBLIC_ORIGIN || url.origin).toString()], + token_endpoint_auth_method: "none", + scopes: OIDConfig.SCOPES, + }), + { + headers: { + "Content-Type": "application/json", + }, + } + ); +}; diff --git a/src/routes/__debug/openai/+server.ts b/src/routes/__debug/openai/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4017ece991d739737c202e05ac27703947491ac --- /dev/null +++ b/src/routes/__debug/openai/+server.ts @@ -0,0 +1,21 @@ +import { json } from "@sveltejs/kit"; +import { config } from "$lib/server/config"; +const DEFAULT_OPENAI_BASE = "https://router.huggingface.co/v1"; + +export async function GET() { + const base = (config.OPENAI_BASE_URL || DEFAULT_OPENAI_BASE).replace(/\/$/, ""); + try { + const res = await fetch(`${base}/models`); + const text = await res.text(); + let length: number | null = null; + try { + const parsed = JSON.parse(text); + length = Array.isArray(parsed?.data) ? parsed.data.length : null; + } catch (_err) { + length = null; // ignore parse errors + } + return json({ base, status: res.status, ok: res.ok, length, sample: text.slice(0, 1000) }); + } catch (e) { + return json({ base, error: String(e) }); + } +} diff --git a/src/routes/admin/export/+server.ts b/src/routes/admin/export/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..d084b9428a15a066ed7657e531bd41340f1d1e7e --- /dev/null +++ b/src/routes/admin/export/+server.ts @@ -0,0 +1,159 @@ +import { config } from "$lib/server/config"; +import { collections } from "$lib/server/database"; +import type { Message } from "$lib/types/Message"; +import { error } from "@sveltejs/kit"; +import { pathToFileURL } from "node:url"; +import { unlink } from "node:fs/promises"; +import { uploadFile } from "@huggingface/hub"; +import parquet from "parquetjs"; +import { z } from "zod"; +import { logger } from "$lib/server/logger.js"; + +// Triger like this: +// curl -X POST "http://localhost:5173/chat/admin/export" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"model": "OpenAssistant/oasst-sft-6-llama-30b-xor"}' + +export async function POST({ request }) { + if (!config.PARQUET_EXPORT_DATASET || !config.PARQUET_EXPORT_HF_TOKEN) { + error(500, "Parquet export is not configured."); + } + + const { model } = z + .object({ + model: z.string(), + }) + .parse(await request.json()); + + const schema = new parquet.ParquetSchema({ + title: { type: "UTF8" }, + created_at: { type: "TIMESTAMP_MILLIS" }, + updated_at: { type: "TIMESTAMP_MILLIS" }, + messages: { + repeated: true, + fields: { + from: { type: "UTF8" }, + content: { type: "UTF8" }, + score: { type: "INT_8", optional: true }, + }, + }, + }); + + const fileName = `/tmp/conversations-${new Date().toJSON().slice(0, 10)}-${Date.now()}.parquet`; + + const writer = await parquet.ParquetWriter.openFile(schema, fileName); + + let count = 0; + logger.info("Exporting conversations for model", model); + + for await (const conversation of collections.settings.aggregate<{ + title: string; + created_at: Date; + updated_at: Date; + messages: Message[]; + }>([ + { + $match: { + shareConversationsWithModelAuthors: true, + sessionId: { $exists: true }, + userId: { $exists: false }, + }, + }, + { + $lookup: { + from: "conversations", + localField: "sessionId", + foreignField: "sessionId", + as: "conversations", + pipeline: [{ $match: { model, userId: { $exists: false } } }], + }, + }, + { $unwind: "$conversations" }, + { + $project: { + title: "$conversations.title", + created_at: "$conversations.createdAt", + updated_at: "$conversations.updatedAt", + messages: "$conversations.messages", + }, + }, + ])) { + await writer.appendRow({ + title: conversation.title, + created_at: conversation.created_at, + updated_at: conversation.updated_at, + messages: conversation.messages.map((message: Message) => ({ + from: message.from, + content: message.content, + ...(message.score ? { score: message.score } : undefined), + })), + }); + ++count; + + if (count % 1_000 === 0) { + logger.info("Exported", count, "conversations"); + } + } + + logger.info("exporting convos with userId"); + + for await (const conversation of collections.settings.aggregate<{ + title: string; + created_at: Date; + updated_at: Date; + messages: Message[]; + }>([ + { $match: { shareConversationsWithModelAuthors: true, userId: { $exists: true } } }, + { + $lookup: { + from: "conversations", + localField: "userId", + foreignField: "userId", + as: "conversations", + pipeline: [{ $match: { model } }], + }, + }, + { $unwind: "$conversations" }, + { + $project: { + title: "$conversations.title", + created_at: "$conversations.createdAt", + updated_at: "$conversations.updatedAt", + messages: "$conversations.messages", + }, + }, + ])) { + await writer.appendRow({ + title: conversation.title, + created_at: conversation.created_at, + updated_at: conversation.updated_at, + messages: conversation.messages.map((message: Message) => ({ + from: message.from, + content: message.content, + ...(message.score ? { score: message.score } : undefined), + })), + }); + ++count; + + if (count % 1_000 === 0) { + logger.info("Exported", count, "conversations"); + } + } + + await writer.close(); + + logger.info("Uploading", fileName, "to Hugging Face Hub"); + + await uploadFile({ + file: pathToFileURL(fileName) as URL, + credentials: { accessToken: config.PARQUET_EXPORT_HF_TOKEN }, + repo: { + type: "dataset", + name: config.PARQUET_EXPORT_DATASET, + }, + }); + + logger.info("Upload done"); + + await unlink(fileName); + + return new Response(); +} diff --git a/src/routes/admin/stats/compute/+server.ts b/src/routes/admin/stats/compute/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef1efe0df17652a0953e3b40b80b5905ab881338 --- /dev/null +++ b/src/routes/admin/stats/compute/+server.ts @@ -0,0 +1,16 @@ +import { json } from "@sveltejs/kit"; +import { logger } from "$lib/server/logger"; +import { computeAllStats } from "$lib/jobs/refresh-conversation-stats"; + +// Triger like this: +// curl -X POST "http://localhost:5173/chat/admin/stats/compute" -H "Authorization: Bearer " + +export async function POST() { + computeAllStats().catch((e) => logger.error(e)); + return json( + { + message: "Stats job started", + }, + { status: 202 } + ); +} diff --git a/src/routes/api/conversation/[id]/+server.ts b/src/routes/api/conversation/[id]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..3763bb1a142c9b140c90aea279fc26a1d049fc72 --- /dev/null +++ b/src/routes/api/conversation/[id]/+server.ts @@ -0,0 +1,40 @@ +import { collections } from "$lib/server/database"; +import { authCondition } from "$lib/server/auth"; +import { z } from "zod"; +import { ObjectId } from "mongodb"; + +export async function GET({ locals, params }) { + const id = z.string().parse(params.id); + const convId = new ObjectId(id); + + if (locals.user?._id || locals.sessionId) { + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (conv) { + const res = { + id: conv._id, + title: conv.title, + updatedAt: conv.updatedAt, + modelId: conv.model, + messages: conv.messages.map((message) => ({ + content: message.content, + from: message.from, + id: message.id, + createdAt: message.createdAt, + updatedAt: message.updatedAt, + // websearch removed + files: message.files, + updates: message.updates, + })), + }; + return Response.json(res); + } else { + return Response.json({ message: "Conversation not found" }, { status: 404 }); + } + } else { + return Response.json({ message: "Must have session cookie" }, { status: 401 }); + } +} diff --git a/src/routes/api/conversation/[id]/message/[messageId]/+server.ts b/src/routes/api/conversation/[id]/message/[messageId]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..80d642ca31274b4c9cbc410cb9f0bd9df64af836 --- /dev/null +++ b/src/routes/api/conversation/[id]/message/[messageId]/+server.ts @@ -0,0 +1,42 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export async function DELETE({ locals, params }) { + const messageId = params.messageId; + + if (!messageId || typeof messageId !== "string") { + error(400, "Invalid message id"); + } + + const conversation = await collections.conversations.findOne({ + ...authCondition(locals), + _id: new ObjectId(params.id), + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + const filteredMessages = conversation.messages + .filter( + (message) => + // not the message AND the message is not in ancestors + !(message.id === messageId) && message.ancestors && !message.ancestors.includes(messageId) + ) + .map((message) => { + // remove the message from children if it's there + if (message.children && message.children.includes(messageId)) { + message.children = message.children.filter((child) => child !== messageId); + } + return message; + }); + + await collections.conversations.updateOne( + { _id: conversation._id, ...authCondition(locals) }, + { $set: { messages: filteredMessages } } + ); + + return new Response(); +} diff --git a/src/routes/api/conversations/+server.ts b/src/routes/api/conversations/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..03d09450432b728dbd77e565591a5eaa5a5b2e44 --- /dev/null +++ b/src/routes/api/conversations/+server.ts @@ -0,0 +1,48 @@ +import { collections } from "$lib/server/database"; +import { authCondition } from "$lib/server/auth"; +import type { Conversation } from "$lib/types/Conversation"; +import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination"; + +export async function GET({ locals, url }) { + const p = parseInt(url.searchParams.get("p") ?? "0"); + if (locals.user?._id || locals.sessionId) { + const convs = await collections.conversations + .find({ + ...authCondition(locals), + }) + .project>({ + title: 1, + updatedAt: 1, + model: 1, + }) + .sort({ updatedAt: -1 }) + .skip(p * CONV_NUM_PER_PAGE) + .limit(CONV_NUM_PER_PAGE) + .toArray(); + + if (convs.length === 0) { + return Response.json([]); + } + const res = convs.map((conv) => ({ + _id: conv._id, + id: conv._id, // legacy param iOS + title: conv.title, + updatedAt: conv.updatedAt, + model: conv.model, + modelId: conv.model, // legacy param iOS + })); + return Response.json(res); + } else { + return Response.json({ message: "Must have session cookie" }, { status: 401 }); + } +} + +export async function DELETE({ locals }) { + if (locals.user?._id || locals.sessionId) { + await collections.conversations.deleteMany({ + ...authCondition(locals), + }); + } + + return new Response(); +} diff --git a/src/routes/api/fetch-url/+server.ts b/src/routes/api/fetch-url/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb9859d5d5f1efd96e169f19f8dc8dfd2d087c9f --- /dev/null +++ b/src/routes/api/fetch-url/+server.ts @@ -0,0 +1,104 @@ +import { error } from "@sveltejs/kit"; +import { logger } from "$lib/server/logger.js"; +import { fetch } from "undici"; + +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB +const FETCH_TIMEOUT = 30000; // 30 seconds + +// Validate URL safety - HTTPS only +function isValidUrl(urlString: string): boolean { + try { + const url = new URL(urlString); + // Only allow HTTPS protocol + if (url.protocol !== "https:") { + return false; + } + // Prevent localhost/private IPs (basic check) + const hostname = url.hostname.toLowerCase(); + if ( + hostname === "localhost" || + hostname.startsWith("127.") || + hostname.startsWith("192.168.") || + hostname.startsWith("172.16.") || + hostname === "[::1]" || + hostname === "0.0.0.0" + ) { + return false; + } + return true; + } catch { + return false; + } +} + +export async function GET({ url }) { + const targetUrl = url.searchParams.get("url"); + + if (!targetUrl) { + logger.warn("Missing 'url' parameter"); + throw error(400, "Missing 'url' parameter"); + } + + if (!isValidUrl(targetUrl)) { + logger.warn({ targetUrl }, "Invalid or unsafe URL (only HTTPS is supported)"); + throw error(400, "Invalid or unsafe URL (only HTTPS is supported)"); + } + + try { + // Fetch with timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT); + + const response = await fetch(targetUrl, { + signal: controller.signal, + headers: { + "User-Agent": "HuggingChat-Attachment-Fetcher/1.0", + }, + }).finally(() => clearTimeout(timeoutId)); + + if (!response.ok) { + logger.error({ targetUrl, response }, `Error fetching URL. Response not ok.`); + throw error(response.status, `Failed to fetch: ${response.statusText}`); + } + + // Check content length if available + const contentLength = response.headers.get("content-length"); + if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) { + throw error(413, "File too large (max 10MB)"); + } + + // Stream the response back + const contentType = response.headers.get("content-type") || "application/octet-stream"; + const contentDisposition = response.headers.get("content-disposition"); + + const headers: HeadersInit = { + "Content-Type": contentType, + "Cache-Control": "public, max-age=3600", + }; + + if (contentDisposition) { + headers["Content-Disposition"] = contentDisposition; + } + + // Get the body as array buffer to check size + const arrayBuffer = await response.arrayBuffer(); + + if (arrayBuffer.byteLength > MAX_FILE_SIZE) { + throw error(413, "File too large (max 10MB)"); + } + + return new Response(arrayBuffer, { headers }); + } catch (err) { + if (err instanceof Error) { + if (err.name === "AbortError") { + logger.error(err, `Request timeout`); + throw error(504, "Request timeout"); + } + + logger.error(err, `Error fetching URL`); + throw error(500, `Failed to fetch URL: ${err.message}`); + } + logger.error(err, `Error fetching URL`); + throw error(500, "Failed to fetch URL."); + } +} diff --git a/src/routes/api/models/+server.ts b/src/routes/api/models/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a1d41c495537ba5cc5ea117ed63b59cd973075d --- /dev/null +++ b/src/routes/api/models/+server.ts @@ -0,0 +1,24 @@ +import { models } from "$lib/server/models"; + +export async function GET() { + const res = models + .filter((m) => m.unlisted == false) + .map((model) => ({ + id: model.id, + name: model.name, + websiteUrl: model.websiteUrl ?? "https://huggingface.co", + modelUrl: model.modelUrl ?? "https://huggingface.co", + // tokenizer removed in this build + datasetName: model.datasetName, + datasetUrl: model.datasetUrl, + displayName: model.displayName, + description: model.description ?? "", + logoUrl: model.logoUrl, + promptExamples: model.promptExamples ?? [], + preprompt: model.preprompt ?? "", + multimodal: model.multimodal ?? false, + unlisted: model.unlisted ?? false, + hasInferenceAPI: model.hasInferenceAPI ?? false, + })); + return Response.json(res); +} diff --git a/src/routes/api/user/+server.ts b/src/routes/api/user/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d848372beb54691954471b42abbc557d9b52726 --- /dev/null +++ b/src/routes/api/user/+server.ts @@ -0,0 +1,15 @@ +export async function GET({ locals }) { + if (locals.user) { + const res = { + id: locals.user._id, + username: locals.user.username, + name: locals.user.name, + email: locals.user.email, + avatarUrl: locals.user.avatarUrl, + hfUserId: locals.user.hfUserId, + }; + + return Response.json(res); + } + return Response.json({ message: "Must be signed in" }, { status: 401 }); +} diff --git a/src/routes/api/user/validate-token/+server.ts b/src/routes/api/user/validate-token/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e931aca63578bcf720a95279ac52d1100c35b9c --- /dev/null +++ b/src/routes/api/user/validate-token/+server.ts @@ -0,0 +1,20 @@ +import { adminTokenManager } from "$lib/server/adminToken"; +import { z } from "zod"; + +const validateTokenSchema = z.object({ + token: z.string(), +}); + +export const POST = async ({ request, locals }) => { + const { success, data } = validateTokenSchema.safeParse(await request.json()); + + if (!success) { + return new Response(JSON.stringify({ error: "Invalid token" }), { status: 400 }); + } + + if (adminTokenManager.checkToken(data.token, locals.sessionId)) { + return new Response(JSON.stringify({ valid: true })); + } + + return new Response(JSON.stringify({ valid: false })); +}; diff --git a/src/routes/api/v2/[...slugs]/+server.ts b/src/routes/api/v2/[...slugs]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0c247015428c2adeaad5ae8885496405251ff68 --- /dev/null +++ b/src/routes/api/v2/[...slugs]/+server.ts @@ -0,0 +1,9 @@ +import { app } from "$api"; + +type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise; + +export const GET: RequestHandler = ({ request }) => app.handle(request); +export const POST: RequestHandler = ({ request }) => app.handle(request); +export const PUT: RequestHandler = ({ request }) => app.handle(request); +export const PATCH: RequestHandler = ({ request }) => app.handle(request); +export const DELETE: RequestHandler = ({ request }) => app.handle(request); diff --git a/src/routes/conversation/+server.ts b/src/routes/conversation/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..55a9a32e3310cb910d561547b7a6b56f14fba503 --- /dev/null +++ b/src/routes/conversation/+server.ts @@ -0,0 +1,115 @@ +import type { RequestHandler } from "./$types"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { error, redirect } from "@sveltejs/kit"; +import { base } from "$app/paths"; +import { z } from "zod"; +import type { Message } from "$lib/types/Message"; +import { models, validateModel } from "$lib/server/models"; +import { v4 } from "uuid"; +import { authCondition } from "$lib/server/auth"; +import { usageLimits } from "$lib/server/usageLimits"; +import { MetricsServer } from "$lib/server/metrics"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const body = await request.text(); + + let title = ""; + + const parsedBody = z + .object({ + fromShare: z.string().optional(), + model: validateModel(models), + preprompt: z.string().optional(), + }) + .safeParse(JSON.parse(body)); + + if (!parsedBody.success) { + error(400, "Invalid request"); + } + const values = parsedBody.data; + + const convCount = await collections.conversations.countDocuments(authCondition(locals)); + + if (usageLimits?.conversations && convCount > usageLimits?.conversations) { + error(429, "You have reached the maximum number of conversations. Delete some to continue."); + } + + const model = models.find((m) => (m.id || m.name) === values.model); + + if (!model) { + error(400, "Invalid model"); + } + + let messages: Message[] = [ + { + id: v4(), + from: "system", + content: values.preprompt ?? "", + createdAt: new Date(), + updatedAt: new Date(), + children: [], + ancestors: [], + }, + ]; + + let rootMessageId: Message["id"] = messages[0].id; + + if (values.fromShare) { + const conversation = await collections.sharedConversations.findOne({ + _id: values.fromShare, + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + // Strip markers from imported titles + title = conversation.title.replace(/<\/?think>/gi, "").trim(); + messages = conversation.messages; + rootMessageId = conversation.rootMessageId ?? rootMessageId; + values.model = conversation.model; + values.preprompt = conversation.preprompt; + } + + if (model.unlisted) { + error(400, "Can't start a conversation with an unlisted model"); + } + + // use provided preprompt or model preprompt + values.preprompt ??= model?.preprompt ?? ""; + + if (messages && messages.length > 0 && messages[0].from === "system") { + messages[0].content = values.preprompt; + } + + const res = await collections.conversations.insertOne({ + _id: new ObjectId(), + // Always store sanitized titles + title: (title || "New Chat").replace(/<\/?think>/gi, "").trim(), + rootMessageId, + messages, + model: values.model, + preprompt: values.preprompt, + createdAt: new Date(), + updatedAt: new Date(), + userAgent: request.headers.get("User-Agent") ?? undefined, + ...(locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId }), + ...(values.fromShare ? { meta: { fromShareId: values.fromShare } } : {}), + }); + + if (MetricsServer.isEnabled()) { + MetricsServer.getMetrics().model.conversationsTotal.inc({ model: values.model }); + } + + return new Response( + JSON.stringify({ + conversationId: res.insertedId.toString(), + }), + { headers: { "Content-Type": "application/json" } } + ); +}; + +export const GET: RequestHandler = async () => { + redirect(302, `${base}/`); +}; diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..1697dfe4590912dd32845c19de06e0979d519bfd --- /dev/null +++ b/src/routes/conversation/[id]/+page.server.ts @@ -0,0 +1,21 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import { loginEnabled } from "$lib/server/auth"; +import { createConversationFromShare } from "$lib/server/conversation"; + +export const load: PageServerLoad = async ({ url, params, locals, request }) => { + /// if logged in and valid share ID, create conversation from share, usually occurs after login + if (loginEnabled && locals.user && params.id.length === 7) { + const leafId = url.searchParams.get("leafId"); + const conversationId = await createConversationFromShare( + params.id, + locals, + request.headers.get("User-Agent") ?? undefined + ); + + return redirect( + 302, + `../conversation/${conversationId}?leafId=${leafId}&fromShare=${params.id}` + ); + } +}; diff --git a/src/routes/conversation/[id]/+page.svelte b/src/routes/conversation/[id]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e34deba43a8c722ea954b0a7f89fd4f9d08a5a80 --- /dev/null +++ b/src/routes/conversation/[id]/+page.svelte @@ -0,0 +1,463 @@ + + + + + + {title} + + + + +{#if showSubscribeModal} + (showSubscribeModal = false)} /> +{/if} diff --git a/src/routes/conversation/[id]/+page.ts b/src/routes/conversation/[id]/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa51e0b84dfd12ddd9e372021e6a767efee74f45 --- /dev/null +++ b/src/routes/conversation/[id]/+page.ts @@ -0,0 +1,18 @@ +import { useAPIClient, handleResponse } from "$lib/APIClient"; +import { UrlDependency } from "$lib/types/UrlDependency"; +import { redirect } from "@sveltejs/kit"; + +export const load = async ({ params, depends, fetch, url }) => { + depends(UrlDependency.Conversation); + + const client = useAPIClient({ fetch, origin: url.origin }); + + try { + return await client + .conversations({ id: params.id }) + .get({ query: { fromShare: url.searchParams.get("fromShare") ?? undefined } }) + .then(handleResponse); + } catch { + redirect(302, "/"); + } +}; diff --git a/src/routes/conversation/[id]/+server.ts b/src/routes/conversation/[id]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b85afefb6f15b8b077fa5b7c64af51275074e0f --- /dev/null +++ b/src/routes/conversation/[id]/+server.ts @@ -0,0 +1,599 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { models, validModelIdSchema } from "$lib/server/models"; +import { ERROR_MESSAGES } from "$lib/stores/errors"; +import type { Message } from "$lib/types/Message"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; +import { + MessageUpdateStatus, + MessageUpdateType, + type MessageUpdate, +} from "$lib/types/MessageUpdate"; +import { uploadFile } from "$lib/server/files/uploadFile"; +import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation"; +import { isMessageId } from "$lib/utils/tree/isMessageId"; +import { buildSubtree } from "$lib/utils/tree/buildSubtree.js"; +import { addChildren } from "$lib/utils/tree/addChildren.js"; +import { addSibling } from "$lib/utils/tree/addSibling.js"; +import { usageLimits } from "$lib/server/usageLimits"; +import { textGeneration } from "$lib/server/textGeneration"; +import type { TextGenerationContext } from "$lib/server/textGeneration/types"; +import { logger } from "$lib/server/logger.js"; +import { AbortRegistry } from "$lib/server/abortRegistry"; +import { MetricsServer } from "$lib/server/metrics"; + +export async function POST({ request, locals, params, getClientAddress }) { + const id = z.string().parse(params.id); + const convId = new ObjectId(id); + const promptedAt = new Date(); + + const userId = locals.user?._id ?? locals.sessionId; + + // check user + if (!userId) { + error(401, "Unauthorized"); + } + + // check if the user has access to the conversation + const convBeforeCheck = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (convBeforeCheck && !convBeforeCheck.rootMessageId) { + const res = await collections.conversations.updateOne( + { + _id: convId, + }, + { + $set: { + ...convBeforeCheck, + ...convertLegacyConversation(convBeforeCheck), + }, + } + ); + + if (!res.acknowledged) { + error(500, "Failed to convert conversation"); + } + } + + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + + // register the event for ratelimiting + await collections.messageEvents.insertOne({ + type: "message", + userId, + createdAt: new Date(), + expiresAt: new Date(Date.now() + 60_000), + ip: getClientAddress(), + }); + + if (usageLimits?.messagesPerMinute) { + // check if the user is rate limited + const nEvents = Math.max( + await collections.messageEvents.countDocuments({ + userId, + type: "message", + expiresAt: { $gt: new Date() }, + }), + await collections.messageEvents.countDocuments({ + ip: getClientAddress(), + type: "message", + expiresAt: { $gt: new Date() }, + }) + ); + if (nEvents > usageLimits.messagesPerMinute) { + error(429, ERROR_MESSAGES.rateLimited); + } + } + + if (usageLimits?.messages && conv.messages.length > usageLimits.messages) { + error( + 429, + `This conversation has more than ${usageLimits.messages} messages. Start a new one to continue` + ); + } + + // fetch the model + const model = models.find((m) => m.id === conv.model); + + if (!model) { + error(410, "Model not available anymore"); + } + + // finally parse the content of the request + const form = await request.formData(); + + const json = form.get("data"); + + if (!json || typeof json !== "string") { + error(400, "Invalid request"); + } + + const { + inputs: newPrompt, + id: messageId, + is_retry: isRetry, + } = z + .object({ + id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue + inputs: z.optional( + z + .string() + .min(1) + .transform((s) => s.replace(/\r\n/g, "\n")) + ), + is_retry: z.optional(z.boolean()), + files: z.optional( + z.array( + z.object({ + type: z.literal("base64").or(z.literal("hash")), + name: z.string(), + value: z.string(), + mime: z.string(), + }) + ) + ), + }) + .parse(JSON.parse(json)); + + const inputFiles = await Promise.all( + form + .getAll("files") + .filter((entry): entry is File => entry instanceof File && entry.size > 0) + .map(async (file) => { + const [type, ...name] = file.name.split(";"); + + return { + type: z.literal("base64").or(z.literal("hash")).parse(type), + value: await file.text(), + mime: file.type, + name: name.join(";"), + }; + }) + ); + + if (usageLimits?.messageLength && (newPrompt?.length ?? 0) > usageLimits.messageLength) { + error(400, "Message too long."); + } + + // each file is either: + // base64 string requiring upload to the server + // hash pointing to an existing file + const hashFiles = inputFiles?.filter((file) => file.type === "hash") ?? []; + const b64Files = + inputFiles + ?.filter((file) => file.type !== "hash") + .map((file) => { + const blob = Buffer.from(file.value, "base64"); + return new File([blob], file.name, { type: file.mime }); + }) ?? []; + + // check sizes + // todo: make configurable + if (b64Files.some((file) => file.size > 10 * 1024 * 1024)) { + error(413, "File too large, should be <10MB"); + } + + const uploadedFiles = await Promise.all(b64Files.map((file) => uploadFile(file, conv))).then( + (files) => [...files, ...hashFiles] + ); + + // we will append tokens to the content of this message + let messageToWriteToId: Message["id"] | undefined = undefined; + // used for building the prompt, subtree of the conversation that goes from the latest message to the root + let messagesForPrompt: Message[] = []; + + if (isRetry && messageId) { + // two cases, if we're retrying a user message with a newPrompt set, + // it means we're editing a user message + // if we're retrying on an assistant message, newPrompt cannot be set + // it means we're retrying the last assistant message for a new answer + + const messageToRetry = conv.messages.find((message) => message.id === messageId); + + if (!messageToRetry) { + error(404, "Message not found"); + } + + if (messageToRetry.from === "user" && newPrompt) { + // add a sibling to this message from the user, with the alternative prompt + // add a children to that sibling, where we can write to + const newUserMessageId = addSibling( + conv, + { + from: "user", + content: newPrompt, + files: uploadedFiles, + createdAt: new Date(), + updatedAt: new Date(), + }, + messageId + ); + messageToWriteToId = addChildren( + conv, + { + from: "assistant", + content: "", + createdAt: new Date(), + updatedAt: new Date(), + }, + newUserMessageId + ); + messagesForPrompt = buildSubtree(conv, newUserMessageId); + } else if (messageToRetry.from === "assistant") { + // we're retrying an assistant message, to generate a new answer + // just add a sibling to the assistant answer where we can write to + messageToWriteToId = addSibling( + conv, + { from: "assistant", content: "", createdAt: new Date(), updatedAt: new Date() }, + messageId + ); + messagesForPrompt = buildSubtree(conv, messageId); + messagesForPrompt.pop(); // don't need the latest assistant message in the prompt since we're retrying it + } + } else { + // just a normal linear conversation, so we add the user message + // and the blank assistant message back to back + const newUserMessageId = addChildren( + conv, + { + from: "user", + content: newPrompt ?? "", + files: uploadedFiles, + createdAt: new Date(), + updatedAt: new Date(), + }, + messageId + ); + + messageToWriteToId = addChildren( + conv, + { + from: "assistant", + content: "", + createdAt: new Date(), + updatedAt: new Date(), + }, + newUserMessageId + ); + // build the prompt from the user message + messagesForPrompt = buildSubtree(conv, newUserMessageId); + } + + const messageToWriteTo = conv.messages.find((message) => message.id === messageToWriteToId); + if (!messageToWriteTo) { + error(500, "Failed to create message"); + } + if (messagesForPrompt.length === 0) { + error(500, "Failed to create prompt"); + } + + // update the conversation with the new messages + await collections.conversations.updateOne( + { _id: convId }, + { $set: { messages: conv.messages, title: conv.title, updatedAt: new Date() } } + ); + + let doneStreaming = false; + let clientDetached = false; + + let lastTokenTimestamp: undefined | Date = undefined; + let firstTokenObserved = false; + const metricsEnabled = MetricsServer.isEnabled(); + const metrics = metricsEnabled ? MetricsServer.getMetrics() : undefined; + const metricsModelId = model.id ?? model.name ?? conv.model; + const metricsLabels = { model: metricsModelId }; + + const persistConversation = async () => { + await collections.conversations.updateOne( + { _id: convId }, + { $set: { messages: conv.messages, title: conv.title, updatedAt: new Date() } } + ); + }; + + const abortRegistry = AbortRegistry.getInstance(); + + // we now build the stream + const stream = new ReadableStream({ + async start(controller) { + const conversationKey = convId.toString(); + const ctrl = new AbortController(); + abortRegistry.register(conversationKey, ctrl); + + let finalAnswerReceived = false; + let abortedByUser = false; + + messageToWriteTo.updates ??= []; + async function update(event: MessageUpdate) { + if (!messageToWriteTo || !conv) { + throw Error("No message or conversation to write events to"); + } + + // Add token to content or skip if empty + if (event.type === MessageUpdateType.Stream) { + if (event.token === "") return; + messageToWriteTo.content += event.token; + + if (metricsEnabled && metrics) { + const now = Date.now(); + metrics.model.tokenCountTotal.inc(metricsLabels); + + if (!firstTokenObserved) { + metrics.model.timeToFirstToken.observe(metricsLabels, now - promptedAt.getTime()); + firstTokenObserved = true; + } + + const previousTimestamp = lastTokenTimestamp + ? lastTokenTimestamp.getTime() + : promptedAt.getTime(); + metrics.model.timePerOutputToken.observe(metricsLabels, now - previousTimestamp); + } + + lastTokenTimestamp = new Date(); + } + + // Set the title + else if (event.type === MessageUpdateType.Title) { + // Always strip markers from titles when saving + const sanitizedTitle = event.title.replace(/<\/?think>/gi, "").trim(); + conv.title = sanitizedTitle; + await collections.conversations.updateOne( + { _id: convId }, + { $set: { title: conv?.title, updatedAt: new Date() } } + ); + } + + // Set the final text and the interrupted flag + else if (event.type === MessageUpdateType.FinalAnswer) { + messageToWriteTo.interrupted = event.interrupted; + messageToWriteTo.content = initialMessageContent + event.text; + finalAnswerReceived = true; + + if (metricsEnabled && metrics) { + metrics.model.latency.observe(metricsLabels, Date.now() - promptedAt.getTime()); + } + } + + // Add file + else if (event.type === MessageUpdateType.File) { + messageToWriteTo.files = [ + ...(messageToWriteTo.files ?? []), + { type: "hash", name: event.name, value: event.sha, mime: event.mime }, + ]; + } + + // Store router metadata (for router models) or provider info (for all models) + else if (event.type === MessageUpdateType.RouterMetadata) { + // Merge metadata updates to preserve existing fields (router may send route/model first, then provider comes later) + if (model?.isRouter) { + messageToWriteTo.routerMetadata = { + route: event.route || messageToWriteTo.routerMetadata?.route || "", + model: event.model || messageToWriteTo.routerMetadata?.model || "", + provider: event.provider || messageToWriteTo.routerMetadata?.provider, + }; + } + // Store provider-only metadata for non-router models if available + else if (event.provider) { + messageToWriteTo.routerMetadata = { + route: messageToWriteTo.routerMetadata?.route || "", + model: messageToWriteTo.routerMetadata?.model || "", + provider: event.provider, + }; + } + } + + // Append to the persistent message updates if it's not a stream update + if ( + event.type !== MessageUpdateType.Stream && + !( + event.type === MessageUpdateType.Status && + event.status === MessageUpdateStatus.KeepAlive + ) + ) { + messageToWriteTo?.updates?.push(event); + } + + // Avoid remote keylogging attack executed by watching packet lengths + // by padding the text with null chars to a fixed length + // https://cdn.arstechnica.net/wp-content/uploads/2024/03/LLM-Side-Channel.pdf + if (event.type === MessageUpdateType.Stream) { + event = { ...event, token: event.token.padEnd(16, "\0") }; + } + + messageToWriteTo.updatedAt = new Date(); + + const enqueueUpdate = async () => { + if (clientDetached) return; + try { + controller.enqueue(JSON.stringify(event) + "\n"); + if (event.type === MessageUpdateType.FinalAnswer) { + controller.enqueue(" ".repeat(4096)); + } + } catch (err) { + clientDetached = true; + logger.info( + { conversationId: convId.toString() }, + "Client detached during message streaming" + ); + } + }; + + await enqueueUpdate(); + + if (clientDetached) { + await persistConversation(); + } + } + + let hasError = false; + const initialMessageContent = messageToWriteTo.content; + + try { + const ctx: TextGenerationContext = { + model, + endpoint: await model.getEndpoint(), + conv, + messages: messagesForPrompt, + assistant: undefined, + promptedAt, + ip: getClientAddress(), + username: locals.user?.username, + // Force-enable multimodal if user settings say so for this model + forceMultimodal: Boolean( + (await collections.settings.findOne(authCondition(locals)))?.multimodalOverrides?.[ + model.id + ] + ), + locals, + abortController: ctrl, + }; + // run the text generation and send updates to the client + for await (const event of textGeneration(ctx)) await update(event); + if (ctrl.signal.aborted) { + abortedByUser = true; + } + if (abortedByUser && !finalAnswerReceived) { + const partialText = messageToWriteTo.content.slice(initialMessageContent.length); + await update({ + type: MessageUpdateType.FinalAnswer, + text: partialText, + interrupted: true, + }); + } + } catch (e) { + const err = e as Error; + const isAbortError = + err?.name === "AbortError" || + err?.name === "APIUserAbortError" || + err?.message === "Request was aborted."; + if (isAbortError || ctrl.signal.aborted) { + abortedByUser = true; + logger.info({ conversationId: conversationKey }, "Generation aborted by user"); + if (!finalAnswerReceived) { + const partialText = messageToWriteTo.content.slice(initialMessageContent.length); + await update({ + type: MessageUpdateType.FinalAnswer, + text: partialText, + interrupted: true, + }); + } + } else { + hasError = true; + // Extract status code if available from HTTPError or APIError + const errObj = err as unknown as Record; + const statusCode = + (typeof errObj.statusCode === "number" ? errObj.statusCode : undefined) || + (typeof errObj.status === "number" ? errObj.status : undefined); + await update({ + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: err.message, + ...(statusCode && { statusCode }), + }); + logger.error(err); + } + } finally { + // check if no output was generated + if (!hasError && !abortedByUser && messageToWriteTo.content === initialMessageContent) { + await update({ + type: MessageUpdateType.Status, + status: MessageUpdateStatus.Error, + message: "No output was generated. Something went wrong.", + }); + } + } + + await persistConversation(); + abortRegistry.unregister(conversationKey, ctrl); + + // used to detect if cancel() is called bc of interrupt or just because the connection closes + doneStreaming = true; + if (!clientDetached) { + controller.close(); + } + }, + async cancel() { + if (doneStreaming) return; + clientDetached = true; + await persistConversation(); + }, + }); + + if (metricsEnabled && metrics) { + metrics.model.messagesTotal.inc(metricsLabels); + } + + // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors + return new Response(stream, { + headers: { + "Content-Type": "application/jsonl", + }, + }); +} + +export async function DELETE({ locals, params }) { + const convId = new ObjectId(params.id); + + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + + await collections.conversations.deleteOne({ _id: conv._id }); + + return new Response(); +} + +export async function PATCH({ request, locals, params }) { + const values = z + .object({ + title: z.string().trim().min(1).max(100).optional(), + model: validModelIdSchema.optional(), + }) + .parse(await request.json()); + + const convId = new ObjectId(params.id); + + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + + // Only include defined values in the update, with title sanitized + const updateValues = { + ...(values.title !== undefined && { + title: values.title.replace(/<\/?think>/gi, "").trim(), + }), + ...(values.model !== undefined && { model: values.model }), + }; + + await collections.conversations.updateOne( + { + _id: convId, + }, + { + $set: updateValues, + } + ); + + return new Response(); +} diff --git a/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..8067b815c3af0f6be27800980918cad035697a78 --- /dev/null +++ b/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts @@ -0,0 +1,66 @@ +import { buildPrompt } from "$lib/buildPrompt"; +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { models } from "$lib/server/models"; +import { buildSubtree } from "$lib/utils/tree/buildSubtree"; +import { isMessageId } from "$lib/utils/tree/isMessageId"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export async function GET({ params, locals }) { + const conv = + params.id.length === 7 + ? await collections.sharedConversations.findOne({ + _id: params.id, + }) + : await collections.conversations.findOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + if (conv === null) { + error(404, "Conversation not found"); + } + + const messageId = params.messageId; + + const messageIndex = conv.messages.findIndex((msg) => msg.id === messageId); + + if (!isMessageId(messageId) || messageIndex === -1) { + error(404, "Message not found"); + } + + const model = models.find((m) => m.id === conv.model); + + if (!model) { + error(404, "Conversation model not found"); + } + + const messagesUpTo = buildSubtree(conv, messageId); + + const prompt = await buildPrompt({ + preprompt: conv.preprompt, + messages: messagesUpTo, + model, + }).catch((err) => { + console.error(err); + return "Prompt generation failed"; + }); + + return Response.json({ + prompt, + model: model.name, + parameters: { + ...model.parameters, + return_full_text: false, + }, + messages: messagesUpTo.map((msg) => ({ + role: msg.from, + content: msg.content, + createdAt: msg.createdAt, + updatedAt: msg.updatedAt, + updates: msg.updates?.filter((u) => u.type === "title"), + files: msg.files, + })), + }); +} diff --git a/src/routes/conversation/[id]/output/[sha256]/+server.ts b/src/routes/conversation/[id]/output/[sha256]/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce78cf9fdf2916cc1c3df49744aa648b9128c304 --- /dev/null +++ b/src/routes/conversation/[id]/output/[sha256]/+server.ts @@ -0,0 +1,58 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; +import type { RequestHandler } from "./$types"; +import { downloadFile } from "$lib/server/files/downloadFile"; +import mimeTypes from "mime-types"; + +export const GET: RequestHandler = async ({ locals, params }) => { + const sha256 = z.string().parse(params.sha256); + + const userId = locals.user?._id ?? locals.sessionId; + + // check user + if (!userId) { + error(401, "Unauthorized"); + } + + if (params.id.length !== 7) { + const convId = new ObjectId(z.string().parse(params.id)); + + // check if the user has access to the conversation + const conv = await collections.conversations.findOne({ + _id: convId, + ...authCondition(locals), + }); + + if (!conv) { + error(404, "Conversation not found"); + } + } else { + // look for the conversation in shared conversations + const conv = await collections.sharedConversations.findOne({ + _id: params.id, + }); + + if (!conv) { + error(404, "Conversation not found"); + } + } + + const { value, mime } = await downloadFile(sha256, params.id); + + const b64Value = Buffer.from(value, "base64"); + return new Response(b64Value, { + headers: { + "Content-Type": mime ?? "application/octet-stream", + "Content-Security-Policy": + "default-src 'none'; script-src 'none'; style-src 'none'; sandbox;", + "Content-Disposition": `attachment; filename="${sha256.slice(0, 8)}.${ + mime ? mimeTypes.extension(mime) || "bin" : "bin" + }"`, + "Content-Length": b64Value.length.toString(), + "Accept-Range": "bytes", + }, + }); +}; diff --git a/src/routes/conversation/[id]/share/+server.ts b/src/routes/conversation/[id]/share/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..f15bd510561a2e05b9f449cd4972528f2ab2fa7d --- /dev/null +++ b/src/routes/conversation/[id]/share/+server.ts @@ -0,0 +1,69 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import type { SharedConversation } from "$lib/types/SharedConversation"; +import { hashConv } from "$lib/utils/hashConv"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { nanoid } from "nanoid"; + +export async function POST({ params, locals }) { + const conversation = await collections.conversations.findOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + const hash = await hashConv(conversation); + + const existingShare = await collections.sharedConversations.findOne({ hash }); + + if (existingShare) { + return new Response( + JSON.stringify({ + shareId: existingShare._id, + }), + { headers: { "Content-Type": "application/json" } } + ); + } + + const shared: SharedConversation = { + _id: nanoid(7), + hash, + createdAt: new Date(), + updatedAt: new Date(), + rootMessageId: conversation.rootMessageId, + messages: conversation.messages, + title: conversation.title, + model: conversation.model, + preprompt: conversation.preprompt, + }; + + await collections.sharedConversations.insertOne(shared); + + // copy files from `${conversation._id}-` to `${shared._id}-` + const files = await collections.bucket + .find({ filename: { $regex: `${conversation._id}-` } }) + .toArray(); + + await Promise.all( + files.map(async (file) => { + const newFilename = file.filename.replace(`${conversation._id}-`, `${shared._id}-`); + // copy files from `${conversation._id}-` to `${shared._id}-` by downloading and reuploaidng + const downloadStream = collections.bucket.openDownloadStream(file._id); + const uploadStream = collections.bucket.openUploadStream(newFilename, { + metadata: { ...file.metadata, conversation: shared._id.toString() }, + }); + downloadStream.pipe(uploadStream); + }) + ); + + return new Response( + JSON.stringify({ + shareId: shared._id, + }), + { headers: { "Content-Type": "application/json" } } + ); +} diff --git a/src/routes/conversation/[id]/stop-generating/+server.ts b/src/routes/conversation/[id]/stop-generating/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..20715bdb877479677988ae7142319ec0174a4c0a --- /dev/null +++ b/src/routes/conversation/[id]/stop-generating/+server.ts @@ -0,0 +1,31 @@ +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { AbortRegistry } from "$lib/server/abortRegistry"; +import { error } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +/** + * Ideally, we'd be able to detect the client-side abort, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850 + */ +export async function POST({ params, locals }) { + const conversationId = new ObjectId(params.id); + + const conversation = await collections.conversations.findOne({ + _id: conversationId, + ...authCondition(locals), + }); + + if (!conversation) { + error(404, "Conversation not found"); + } + + AbortRegistry.getInstance().abort(conversationId.toString()); + + await collections.abortedGenerations.updateOne( + { conversationId }, + { $set: { updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } }, + { upsert: true } + ); + + return new Response(); +} diff --git a/src/routes/healthcheck/+server.ts b/src/routes/healthcheck/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..edb40a0dd81f9f7014376f129630dd5f5ab4f095 --- /dev/null +++ b/src/routes/healthcheck/+server.ts @@ -0,0 +1,3 @@ +export async function GET() { + return new Response("OK", { status: 200 }); +} diff --git a/src/routes/login/+server.ts b/src/routes/login/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..5cb5b71861f9bd914dc546ba06c3dc0dfbbd74d7 --- /dev/null +++ b/src/routes/login/+server.ts @@ -0,0 +1,5 @@ +import { triggerOauthFlow } from "$lib/server/auth"; + +export async function GET({ request, url, locals }) { + return await triggerOauthFlow({ request, url, locals }); +} diff --git a/src/routes/login/callback/+server.ts b/src/routes/login/callback/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ec17500d4e6fed303901d4e1b291aca1c2fdf92 --- /dev/null +++ b/src/routes/login/callback/+server.ts @@ -0,0 +1,97 @@ +import { error, redirect } from "@sveltejs/kit"; +import { getOIDCUserData, validateAndParseCsrfToken } from "$lib/server/auth"; +import { z } from "zod"; +import { base } from "$app/paths"; +import { config } from "$lib/server/config"; +import JSON5 from "json5"; +import { updateUser } from "./updateUser.js"; + +const sanitizeJSONEnv = (val: string, fallback: string) => { + const raw = (val ?? "").trim(); + const unquoted = raw.startsWith("`") && raw.endsWith("`") ? raw.slice(1, -1) : raw; + return unquoted || fallback; +}; + +const allowedUserEmails = z + .array(z.string().email()) + .optional() + .default([]) + .parse(JSON5.parse(sanitizeJSONEnv(config.ALLOWED_USER_EMAILS, "[]"))); + +const allowedUserDomains = z + .array(z.string().regex(/\.\w+$/)) // Contains at least a dot + .optional() + .default([]) + .parse(JSON5.parse(sanitizeJSONEnv(config.ALLOWED_USER_DOMAINS, "[]"))); + +export async function GET({ url, locals, cookies, request, getClientAddress }) { + const { error: errorName, error_description: errorDescription } = z + .object({ + error: z.string().optional(), + error_description: z.string().optional(), + }) + .parse(Object.fromEntries(url.searchParams.entries())); + + if (errorName) { + throw error(400, errorName + (errorDescription ? ": " + errorDescription : "")); + } + + const { code, state, iss } = z + .object({ + code: z.string(), + state: z.string(), + iss: z.string().optional(), + }) + .parse(Object.fromEntries(url.searchParams.entries())); + + const csrfToken = Buffer.from(state, "base64").toString("utf-8"); + + const validatedToken = await validateAndParseCsrfToken(csrfToken, locals.sessionId); + + if (!validatedToken) { + throw error(403, "Invalid or expired CSRF token"); + } + + const { userData, token } = await getOIDCUserData( + { redirectURI: validatedToken.redirectUrl }, + code, + iss, + url + ); + + // Filter by allowed user emails or domains + if (allowedUserEmails.length > 0 || allowedUserDomains.length > 0) { + if (!userData.email) { + throw error(403, "User not allowed: email not returned"); + } + const emailVerified = userData.email_verified ?? true; + if (!emailVerified) { + throw error(403, "User not allowed: email not verified"); + } + + const emailDomain = userData.email.split("@")[1]; + const isEmailAllowed = allowedUserEmails.includes(userData.email); + const isDomainAllowed = allowedUserDomains.includes(emailDomain); + + if (!isEmailAllowed && !isDomainAllowed) { + throw error(403, "User not allowed"); + } + } + + await updateUser({ + userData, + token, + locals, + cookies, + userAgent: request.headers.get("user-agent") ?? undefined, + ip: getClientAddress(), + }); + + // Prefer returning the user to their original in-app path when provided. + // `validatedToken.next` is sanitized server-side to avoid protocol-relative redirects. + const next = validatedToken.next; + if (next) { + return redirect(302, next); + } + return redirect(302, `${base}/`); +} diff --git a/src/routes/login/callback/updateUser.spec.ts b/src/routes/login/callback/updateUser.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..6273395907107556635cf0e571e8f7ba1da35dc7 --- /dev/null +++ b/src/routes/login/callback/updateUser.spec.ts @@ -0,0 +1,157 @@ +import { assert, it, describe, afterEach, vi, expect } from "vitest"; +import type { Cookies } from "@sveltejs/kit"; +import { collections } from "$lib/server/database"; +import { updateUser } from "./updateUser"; +import { ObjectId } from "mongodb"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { defaultModel } from "$lib/server/models"; +import { findUser } from "$lib/server/auth"; +import type { TokenSet } from "openid-client"; + +const userData = { + preferred_username: "new-username", + name: "name", + picture: "https://example.com/avatar.png", + sub: "1234567890", +}; +Object.freeze(userData); + +const locals = { + userId: "1234567890", + sessionId: "1234567890", + isAdmin: false, +}; + +const token = { + access_token: "access_token", + refresh_token: "refresh_token", + expires_at: Math.floor(Date.now() / 1000) + 3600, // Expires 1 hour from now + expires_in: 3600, +} as TokenSet; + +// @ts-expect-error SvelteKit cookies dumb mock +const cookiesMock: Cookies = { + set: vi.fn(), +}; + +const insertRandomUser = async () => { + const res = await collections.users.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username: "base-username", + name: userData.name, + avatarUrl: userData.picture, + hfUserId: userData.sub, + }); + + return res.insertedId; +}; + +const insertRandomConversations = async (count: number) => { + const res = await collections.conversations.insertMany( + new Array(count).fill(0).map(() => ({ + _id: new ObjectId(), + title: "random title", + messages: [], + model: defaultModel.id, + // embedding model removed in this build + createdAt: new Date(), + updatedAt: new Date(), + sessionId: locals.sessionId, + })) + ); + + return res.insertedIds; +}; + +describe("login", () => { + it("should update user if existing", async () => { + await insertRandomUser(); + + await updateUser({ userData, locals, cookies: cookiesMock, token }); + + const existingUser = await collections.users.findOne({ hfUserId: userData.sub }); + + assert.equal(existingUser?.name, userData.name); + + expect(cookiesMock.set).toBeCalledTimes(1); + }); + + it("should migrate pre-existing conversations for new user", async () => { + const insertedId = await insertRandomUser(); + + await insertRandomConversations(2); + + await updateUser({ userData, locals, cookies: cookiesMock, token }); + + const conversationCount = await collections.conversations.countDocuments({ + userId: insertedId, + sessionId: { $exists: false }, + }); + + assert.equal(conversationCount, 2); + + await collections.conversations.deleteMany({ userId: insertedId }); + }); + + it("should create default settings for new user", async () => { + await updateUser({ userData, locals, cookies: cookiesMock, token }); + + // updateUser creates a new sessionId, so we need to use the updated value + const user = (await findUser(locals.sessionId)).user; + + assert.exists(user); + + const settings = await collections.settings.findOne({ userId: user?._id }); + + expect(settings).toMatchObject({ + userId: user?._id, + updatedAt: expect.any(Date), + createdAt: expect.any(Date), + ...DEFAULT_SETTINGS, + }); + + await collections.settings.deleteOne({ userId: user?._id }); + }); + + it("should migrate pre-existing settings for pre-existing user", async () => { + const { insertedId } = await collections.settings.insertOne({ + sessionId: locals.sessionId, + updatedAt: new Date(), + createdAt: new Date(), + ...DEFAULT_SETTINGS, + shareConversationsWithModelAuthors: false, + }); + + await updateUser({ userData, locals, cookies: cookiesMock, token }); + + const settings = await collections.settings.findOne({ + _id: insertedId, + sessionId: { $exists: false }, + }); + + assert.exists(settings); + + const user = await collections.users.findOne({ hfUserId: userData.sub }); + + expect(settings).toMatchObject({ + userId: user?._id, + updatedAt: expect.any(Date), + createdAt: expect.any(Date), + ...DEFAULT_SETTINGS, + shareConversationsWithModelAuthors: false, + }); + + await collections.settings.deleteOne({ userId: user?._id }); + }); +}); + +afterEach(async () => { + await collections.users.deleteMany({ hfUserId: userData.sub }); + await collections.sessions.deleteMany({}); + + locals.userId = "1234567890"; + locals.sessionId = "1234567890"; + vi.clearAllMocks(); +}); diff --git a/src/routes/login/callback/updateUser.ts b/src/routes/login/callback/updateUser.ts new file mode 100644 index 0000000000000000000000000000000000000000..b16046515475de5f2b1c91c149b3491210dbca7d --- /dev/null +++ b/src/routes/login/callback/updateUser.ts @@ -0,0 +1,215 @@ +import { + getCoupledCookieHash, + refreshSessionCookie, + tokenSetToSessionOauth, +} from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { DEFAULT_SETTINGS } from "$lib/types/Settings"; +import { z } from "zod"; +import type { UserinfoResponse, TokenSet } from "openid-client"; +import { error, type Cookies } from "@sveltejs/kit"; +import crypto from "crypto"; +import { sha256 } from "$lib/utils/sha256"; +import { addWeeks } from "date-fns"; +import { OIDConfig } from "$lib/server/auth"; +import { config } from "$lib/server/config"; +import { logger } from "$lib/server/logger"; + +export async function updateUser(params: { + userData: UserinfoResponse; + token: TokenSet; + locals: App.Locals; + cookies: Cookies; + userAgent?: string; + ip?: string; +}) { + const { userData, token, locals, cookies, userAgent, ip } = params; + + // Microsoft Entra v1 tokens do not provide preferred_username, instead the username is provided in the upn + // claim. See https://learn.microsoft.com/en-us/entra/identity-platform/access-token-claims-reference + if (!userData.preferred_username && userData.upn) { + userData.preferred_username = userData.upn as string; + } + + const { + preferred_username: username, + name, + email, + picture: avatarUrl, + sub: hfUserId, + orgs, + } = z + .object({ + preferred_username: z.string().optional(), + name: z.string(), + picture: z.string().optional(), + sub: z.string(), + email: z.string().email().optional(), + orgs: z + .array( + z.object({ + sub: z.string(), + name: z.string(), + picture: z.string(), + preferred_username: z.string(), + isEnterprise: z.boolean(), + }) + ) + .optional(), + }) + .setKey(OIDConfig.NAME_CLAIM, z.string()) + .refine((data) => data.preferred_username || data.email, { + message: "Either preferred_username or email must be provided by the provider.", + }) + .transform((data) => ({ + ...data, + name: data[OIDConfig.NAME_CLAIM], + })) + .parse(userData) as { + preferred_username?: string; + email?: string; + picture?: string; + sub: string; + name: string; + orgs?: Array<{ + sub: string; + name: string; + picture: string; + preferred_username: string; + isEnterprise: boolean; + }>; + } & Record; + + // Dynamically access user data based on NAME_CLAIM from environment + // This approach allows us to adapt to different OIDC providers flexibly. + + logger.info( + { + login_username: username, + login_name: name, + login_email: email, + login_orgs: orgs?.map((el) => el.sub), + }, + "user login" + ); + // if using huggingface as auth provider, check orgs for earl access and amin rights + const isAdmin = + (config.HF_ORG_ADMIN && orgs?.some((org) => org.sub === config.HF_ORG_ADMIN)) || false; + const isEarlyAccess = + (config.HF_ORG_EARLY_ACCESS && orgs?.some((org) => org.sub === config.HF_ORG_EARLY_ACCESS)) || + false; + + logger.debug( + { + isAdmin, + isEarlyAccess, + hfUserId, + }, + `Updating user ${hfUserId}` + ); + + // check if user already exists + const existingUser = await collections.users.findOne({ hfUserId }); + let userId = existingUser?._id; + + // update session cookie on login + const previousSessionId = locals.sessionId; + const secretSessionId = crypto.randomUUID(); + const sessionId = await sha256(secretSessionId); + + if (await collections.sessions.findOne({ sessionId })) { + error(500, "Session ID collision"); + } + + locals.sessionId = sessionId; + + // Get cookie hash if coupling is enabled + const coupledCookieHash = await getCoupledCookieHash({ type: "svelte", value: cookies }); + + // Prepare OAuth token data for session storage + const oauthData = tokenSetToSessionOauth(token); + + if (existingUser) { + // update existing user if any + await collections.users.updateOne( + { _id: existingUser._id }, + { $set: { username, name, avatarUrl, isAdmin, isEarlyAccess } } + ); + + // remove previous session if it exists and add new one + await collections.sessions.deleteOne({ sessionId: previousSessionId }); + await collections.sessions.insertOne({ + _id: new ObjectId(), + sessionId: locals.sessionId, + userId: existingUser._id, + createdAt: new Date(), + updatedAt: new Date(), + userAgent, + ip, + expiresAt: addWeeks(new Date(), 2), + ...(coupledCookieHash ? { coupledCookieHash } : {}), + ...(oauthData ? { oauth: oauthData } : {}), + }); + } else { + // user doesn't exist yet, create a new one + const { insertedId } = await collections.users.insertOne({ + _id: new ObjectId(), + createdAt: new Date(), + updatedAt: new Date(), + username, + name, + email, + avatarUrl, + hfUserId, + isAdmin, + isEarlyAccess, + }); + + userId = insertedId; + + await collections.sessions.insertOne({ + _id: new ObjectId(), + sessionId: locals.sessionId, + userId, + createdAt: new Date(), + updatedAt: new Date(), + userAgent, + ip, + expiresAt: addWeeks(new Date(), 2), + ...(coupledCookieHash ? { coupledCookieHash } : {}), + ...(oauthData ? { oauth: oauthData } : {}), + }); + + // move pre-existing settings to new user + const { matchedCount } = await collections.settings.updateOne( + { sessionId: previousSessionId }, + { + $set: { userId, updatedAt: new Date() }, + $unset: { sessionId: "" }, + } + ); + + if (!matchedCount) { + // if no settings found for user, create default settings + await collections.settings.insertOne({ + userId, + updatedAt: new Date(), + createdAt: new Date(), + ...DEFAULT_SETTINGS, + }); + } + } + + // refresh session cookie + refreshSessionCookie(cookies, secretSessionId); + + // migrate pre-existing conversations + await collections.conversations.updateMany( + { sessionId: previousSessionId }, + { + $set: { userId }, + $unset: { sessionId: "" }, + } + ); +} diff --git a/src/routes/logout/+server.ts b/src/routes/logout/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5e132d60ead394ae27f85870903dc876a124f6d --- /dev/null +++ b/src/routes/logout/+server.ts @@ -0,0 +1,18 @@ +import { dev } from "$app/environment"; +import { base } from "$app/paths"; +import { collections } from "$lib/server/database"; +import { redirect } from "@sveltejs/kit"; +import { config } from "$lib/server/config"; + +export async function POST({ locals, cookies }) { + await collections.sessions.deleteOne({ sessionId: locals.sessionId }); + + cookies.delete(config.COOKIE_NAME, { + path: "/", + // So that it works inside the space's iframe + sameSite: dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none", + secure: !dev && !(config.ALLOW_INSECURE_COOKIES === "true"), + httpOnly: true, + }); + return redirect(302, `${base}/`); +} diff --git a/src/routes/metrics/+server.ts b/src/routes/metrics/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8ea1512c4650ea402b8ff2c7dd32a243e57d4a5 --- /dev/null +++ b/src/routes/metrics/+server.ts @@ -0,0 +1,18 @@ +import { config } from "$lib/server/config"; +import { MetricsServer } from "$lib/server/metrics"; + +export async function GET() { + if (config.METRICS_ENABLED !== "true") { + return new Response("Not Found", { status: 404 }); + } + + const payload = await MetricsServer.getInstance().render(); + + return new Response(payload, { + status: 200, + headers: { + "Content-Type": "text/plain; version=0.0.4", + "Cache-Control": "no-store", + }, + }); +} diff --git a/src/routes/models/+page.svelte b/src/routes/models/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2180111145f279b640b65c9d90529da552a193ac --- /dev/null +++ b/src/routes/models/+page.svelte @@ -0,0 +1,171 @@ + + + + {#if publicConfig.isHuggingChat} + HuggingChat - Models + + + + + {/if} + + +
+
+
+

Models

+ {#if publicConfig.isHuggingChat} + + + + {/if} +
+

+ All models available{#if publicConfig.isHuggingChat} via Inference Providers{/if} +

+ + + + +
+
+ + diff --git a/src/routes/models/[...model]/+page.svelte b/src/routes/models/[...model]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e61030c0b56e801b84904ee348cbd9a696c4df02 --- /dev/null +++ b/src/routes/models/[...model]/+page.svelte @@ -0,0 +1,133 @@ + + + + + + + + + + + + createConversation(message)} + {loading} + currentModel={findCurrentModel(data.models, data.oldModels, modelId)} + models={data.models} + bind:files + bind:draft +/> diff --git a/src/routes/models/[...model]/+page.ts b/src/routes/models/[...model]/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3eefaa311d200079ab8e5e2f7e19b6a02432a84 --- /dev/null +++ b/src/routes/models/[...model]/+page.ts @@ -0,0 +1,19 @@ +import { base } from "$app/paths"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ params, parent, fetch }) { + const r = await fetch(`${base}/api/v2/models/${params.model}/subscribe`, { + method: "POST", + }); + + if (!r.ok) { + redirect(302, base + "/"); + } + + return { + settings: await parent().then((data) => ({ + ...data.settings, + activeModel: params.model, + })), + }; +} diff --git a/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e8be96333485ac274a8091f6522896bbae76126c --- /dev/null +++ b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte @@ -0,0 +1,28 @@ + + +
+

+ {name.split("/")[1]} +

+ + {#if isHuggingChat} +
+
Chat with it on
+ + {@html logo} +
+ {/if} +
diff --git a/src/routes/privacy/+page.svelte b/src/routes/privacy/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f50fa73a6017b02725a3f941ee796c56ae8d8b6d --- /dev/null +++ b/src/routes/privacy/+page.svelte @@ -0,0 +1,11 @@ + + +
+
+ + {@html marked(privacy, { gfm: true })} +
+
diff --git a/src/routes/r/[id]/+page.server.ts b/src/routes/r/[id]/+page.server.ts new file mode 100644 index 0000000000000000000000000000000000000000..b82528c643fc70d93601468b8e928b2cead3be4c --- /dev/null +++ b/src/routes/r/[id]/+page.server.ts @@ -0,0 +1,23 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import { loginEnabled } from "$lib/server/auth"; +import { createConversationFromShare } from "$lib/server/conversation"; + +export const load: PageServerLoad = async ({ url, params, locals, request }) => { + const leafId = url.searchParams.get("leafId"); + + /// if logged in and valid share ID, create conversation from share + if (loginEnabled && locals.user && params.id) { + const conversationId = await createConversationFromShare( + params.id, + locals, + request.headers.get("User-Agent") ?? undefined + ); + + return redirect( + 302, + `../conversation/${conversationId}?leafId=${leafId}&fromShare=${params.id}` + ); + } + return redirect(302, `../conversation/${params.id}?leafId=${leafId}`); +}; diff --git a/src/routes/settings/(nav)/+layout.svelte b/src/routes/settings/(nav)/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4e5994b8b89605fc46b074456ed42cce5baf3280 --- /dev/null +++ b/src/routes/settings/(nav)/+layout.svelte @@ -0,0 +1,224 @@ + + +
+
+ {#if showContent && browser} + + {/if} +

Settings

+ +
+ {#if !(showContent && browser && !isDesktop(window))} +
+ +

+ Models +

+ + +
+ +
+ + {#each data.models + .filter((el) => !el.unlisted) + .filter((el) => { + const haystack = normalize(`${el.id} ${el.name ?? ""} ${el.displayName ?? ""}`); + return queryTokens.every((q) => haystack.includes(q)); + }) as model} + + {/each} + + +
+ {/if} + {#if showContent} +
+ {@render children?.()} +
+ {/if} +
diff --git a/src/routes/settings/(nav)/+layout.ts b/src/routes/settings/(nav)/+layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3d15781a772c9c833d435893cc10dc9999f63c2 --- /dev/null +++ b/src/routes/settings/(nav)/+layout.ts @@ -0,0 +1 @@ +export const ssr = false; diff --git a/src/routes/settings/(nav)/+page.svelte b/src/routes/settings/(nav)/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/routes/settings/(nav)/+server.ts b/src/routes/settings/(nav)/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..3222cb58d3ab12f1317a2a3cad92a6f49937b535 --- /dev/null +++ b/src/routes/settings/(nav)/+server.ts @@ -0,0 +1,42 @@ +import { collections } from "$lib/server/database"; +import { z } from "zod"; +import { authCondition } from "$lib/server/auth"; +import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings"; + +export async function POST({ request, locals }) { + const body = await request.json(); + + const { welcomeModalSeen, ...settings } = z + .object({ + shareConversationsWithModelAuthors: z + .boolean() + .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors), + welcomeModalSeen: z.boolean().optional(), + activeModel: z.string().default(DEFAULT_SETTINGS.activeModel), + customPrompts: z.record(z.string()).default({}), + multimodalOverrides: z.record(z.boolean()).default({}), + disableStream: z.boolean().default(false), + directPaste: z.boolean().default(false), + hidePromptExamples: z.record(z.boolean()).default({}), + }) + .parse(body) satisfies SettingsEditable; + + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + ...settings, + ...(welcomeModalSeen && { welcomeModalSeenAt: new Date() }), + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + // return ok response + return new Response(); +} diff --git a/src/routes/settings/(nav)/[...model]/+page.svelte b/src/routes/settings/(nav)/[...model]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ca8285ce68a354f68dba9d86273287c11061bc04 --- /dev/null +++ b/src/routes/settings/(nav)/[...model]/+page.svelte @@ -0,0 +1,257 @@ + + +
+
+

+ {model.displayName} +

+ + {#if model.description} +

+ {model.description} +

+ {/if} +
+ + +
+ + + {#if model.modelUrl} + + + Model page + + {/if} + + {#if model.datasetName || model.datasetUrl} + + + Dataset page + + {/if} + + {#if model.websiteUrl} + + + Model website + + {/if} + + {#if publicConfig.isHuggingChat} + {#if !model?.isRouter} + + + API Playground + + + + View model card + + {/if} + +
+ Copy new chat link +
+
+ {/if} +
+ +
+ {#if model?.isRouter} +

+ Omni routes your messages to the best underlying model + depending on your request. +

+ {/if} +
+

System Prompt

+ {#if hasCustomPreprompt} + + {/if} +
+ + + +
+
+
+
+
+ Multimodal support (image inputs) +
+

+ Enable image uploads and send images to this model. +

+
+ +
+ + {#if model?.isRouter} +
+
+
+ Hide prompt examples +
+

+ Hide the prompt suggestions above the chat input. +

+
+ +
+ {/if} +
+
+ + {#if model.providers?.length} +
+
+
+ Inference Providers +
+

+ Providers serving this model. You can enable/disable some providers from your settings. +

+
+
    + {#each providerList as prov, i (prov.provider || i)} + {@const hubOrg = PROVIDERS_HUB_ORGS[prov.provider as keyof typeof PROVIDERS_HUB_ORGS]} +
  • + + {#if hubOrg} + {prov.provider} logo ((e.currentTarget as HTMLImageElement).style.display = "none")} + /> + {/if} + {prov.provider} + +
  • + {/each} +
+
+ {/if} + +
+
diff --git a/src/routes/settings/(nav)/[...model]/+page.ts b/src/routes/settings/(nav)/[...model]/+page.ts new file mode 100644 index 0000000000000000000000000000000000000000..57f70b7da73166bdf3cebdbc4560225613c65624 --- /dev/null +++ b/src/routes/settings/(nav)/[...model]/+page.ts @@ -0,0 +1,14 @@ +import { base } from "$app/paths"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ parent, params }) { + const data = await parent(); + + const model = data.models.find((m: { id: string }) => m.id === params.model); + + if (!model || model.unlisted) { + redirect(302, `${base}/settings`); + } + + return data; +} diff --git a/src/routes/settings/(nav)/application/+page.svelte b/src/routes/settings/(nav)/application/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cf57ff682e9dfe22257390b2e5f9e7898c415d0f --- /dev/null +++ b/src/routes/settings/(nav)/application/+page.svelte @@ -0,0 +1,253 @@ + + +
+

+ Application Settings +

+ + {#if OPENAI_BASE_URL !== null} +
+ API Base URL: + {OPENAI_BASE_URL} +
+ {/if} + {#if !!publicConfig.PUBLIC_COMMIT_SHA} + + {/if} + {#if page.data.isAdmin} +
+

+ Admin mode +

+ + {#if refreshMessage} + {refreshMessage} + {/if} +
+ {/if} +
+
+
+ {#if publicConfig.PUBLIC_APP_DATA_SHARING === "1"} +
+
+
+ Share with model authors +
+

+ Sharing your data helps improve open models over time. +

+
+ +
+ {/if} + +
+
+
+ Disable streaming tokens +
+

+ Show responses only when complete. +

+
+ +
+ +
+
+
+ Paste text directly +
+

+ Paste long text directly into chat instead of a file. +

+
+ +
+ + +
+
+
Theme
+

+ Choose light, dark, or follow system. +

+
+
+ + + +
+
+
+
+ +
+ {#if publicConfig.isHuggingChat} + Github repository + Providers settings + Share your feedback on HuggingChat + About & Privacy + {/if} + +
+
+
diff --git a/src/routes/settings/+layout.svelte b/src/routes/settings/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..243b547e17cccee491d61857dad4c2202c52f2ab --- /dev/null +++ b/src/routes/settings/+layout.svelte @@ -0,0 +1,40 @@ + + + goto(previousPage)} + disableFly={true} + width="border dark:border-gray-700 h-[95dvh] w-[90dvw] pb-0 overflow-hidden rounded-2xl bg-white shadow-2xl outline-none dark:bg-gray-800 dark:text-gray-200 sm:h-[95dvh] xl:w-[1200px] xl:h-[85dvh] 2xl:h-[75dvh]" +> + {@render children?.()} + {#if $settings.recentlySaved} +
+ + Saved +
+ {/if} +
diff --git a/src/styles/highlight-js.css b/src/styles/highlight-js.css new file mode 100644 index 0000000000000000000000000000000000000000..77da96a8dca4344174f0ce4835afa9c20623847e --- /dev/null +++ b/src/styles/highlight-js.css @@ -0,0 +1,195 @@ +/* Atom One Light (v9.16.2) */ +/* + +Atom One Light by Daniel Gamage +Original One Light Syntax theme from https://github.com/atom/one-light-syntax + +base: #fafafa +mono-1: #383a42 +mono-2: #686b77 +mono-3: #a0a1a7 +hue-1: #0184bb +hue-2: #4078f2 +hue-3: #a626a4 +hue-4: #50a14f +hue-5: #e45649 +hue-5-2: #c91243 +hue-6: #986801 +hue-6-2: #c18401 + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #383a42; + background: #fafafa; +} + +.hljs-comment, +.hljs-quote { + color: #a0a1a7; + font-style: italic; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-formula { + color: #a626a4; +} + +.hljs-section, +.hljs-name, +.hljs-selector-tag, +.hljs-deletion, +.hljs-subst { + color: #e45649; +} + +.hljs-literal { + color: #0184bb; +} + +.hljs-string, +.hljs-regexp, +.hljs-addition, +.hljs-attribute, +.hljs-meta-string { + color: #50a14f; +} + +.hljs-built_in, +.hljs-class .hljs-title { + color: #c18401; +} + +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-type, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-number { + color: #986801; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-meta, +.hljs-selector-id, +.hljs-title { + color: #4078f2; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} + +/* Atom One Dark (v9.16.2) scoped to .dark */ +/* + +Atom One Dark by Daniel Gamage +Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax + +base: #282c34 +mono-1: #abb2bf +mono-2: #818896 +mono-3: #5c6370 +hue-1: #56b6c2 +hue-2: #61aeee +hue-3: #c678dd +hue-4: #98c379 +hue-5: #e06c75 +hue-5-2: #be5046 +hue-6: #d19a66 +hue-6-2: #e6c07b + +*/ + +.dark .hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #abb2bf; + background: #282c34; +} + +.dark .hljs-comment, +.dark .hljs-quote { + color: #5c6370; + font-style: italic; +} + +.dark .hljs-doctag, +.dark .hljs-keyword, +.dark .hljs-formula { + color: #c678dd; +} + +.dark .hljs-section, +.dark .hljs-name, +.dark .hljs-selector-tag, +.dark .hljs-deletion, +.dark .hljs-subst { + color: #e06c75; +} + +.dark .hljs-literal { + color: #56b6c2; +} + +.dark .hljs-string, +.dark .hljs-regexp, +.dark .hljs-addition, +.dark .hljs-attribute, +.dark .hljs-meta-string { + color: #98c379; +} + +.dark .hljs-built_in, +.dark .hljs-class .hljs-title { + color: #e6c07b; +} + +.dark .hljs-attr, +.dark .hljs-variable, +.dark .hljs-template-variable, +.dark .hljs-type, +.dark .hljs-selector-class, +.dark .hljs-selector-attr, +.dark .hljs-selector-pseudo, +.dark .hljs-number { + color: #d19a66; +} + +.dark .hljs-symbol, +.dark .hljs-bullet, +.dark .hljs-link, +.dark .hljs-meta, +.dark .hljs-selector-id, +.dark .hljs-title { + color: #61aeee; +} + +.dark .hljs-emphasis { + font-style: italic; +} + +.dark .hljs-strong { + font-weight: bold; +} + +.dark .hljs-link { + text-decoration: underline; +} diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000000000000000000000000000000000000..ed60e23f2d8a18d48dd8cde6232fab9e5c3d9087 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,120 @@ +@import "./highlight-js.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .btn { + @apply inline-flex flex-shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap outline-none transition-all focus:ring disabled:cursor-default; + } + + .active-model { + /* Ensure active border wins over defaults/utilities in both themes */ + @apply !border-black dark:!border-white/60; + } + + .file-hoverable { + @apply hover:bg-gray-500/10; + } + + .base-tool { + @apply flex h-[1.6rem] items-center gap-[.2rem] whitespace-nowrap border border-transparent text-xs outline-none transition-all focus:outline-none active:outline-none dark:hover:text-gray-300 sm:hover:text-purple-600; + } + + .active-tool { + @apply rounded-full !border-purple-200 bg-purple-100 pl-1 pr-2 text-purple-600 hover:text-purple-600 dark:!border-purple-700 dark:bg-purple-600/40 dark:text-purple-200; + } +} + +@layer utilities { + /* your existing utilities */ + .scrollbar-custom { + @apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-black/10 scrollbar-thumb-rounded-full scrollbar-w-1 hover:scrollbar-thumb-black/20 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20; + } + + .scrollbar-custom::-webkit-scrollbar { + background-color: transparent; + width: 8px; + height: 8px; + } + + .dark .scrollbar-custom::-webkit-scrollbar { + background-color: rgba(17, 17, 17, 0.85); + } + + /* Rounded top/bottom caps for vertical scrollbars (Chrome/Edge/Safari) */ + .scrollbar-custom::-webkit-scrollbar-track { + @apply rounded-full bg-clip-padding; /* clip bg to padding so caps look round */ + /* space for the end caps — tweak with Tailwind spacing */ + border-top: theme("spacing.2") solid transparent; /* 0.5rem */ + border-bottom: theme("spacing.2") solid transparent; /* 0.5rem */ + } + + /* Rounded left/right caps for horizontal scrollbars */ + .scrollbar-custom::-webkit-scrollbar-track:horizontal { + @apply rounded-full bg-clip-padding; + border-left: theme("spacing.2") solid transparent; + border-right: theme("spacing.2") solid transparent; + border-top-width: 0; + border-bottom-width: 0; + } + + .no-scrollbar { + @apply [-ms-overflow-style:none] [scrollbar-width:none] [&::-ms-scrollbar]:hidden [&::-webkit-scrollbar]:hidden; + } + + .prose table { + @apply block max-w-full overflow-x-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-black/10 scrollbar-thumb-rounded-full scrollbar-w-1 hover:scrollbar-thumb-black/20 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20; + } + + /* .scrollbar-custom { + @apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-black/10 scrollbar-thumb-rounded-full scrollbar-w-1 hover:scrollbar-thumb-black/20 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20; + } */ + .prose hr { + @apply my-4; + } + + .prose strong { + @apply font-medium; + } + + .prose pre { + @apply border-[0.5px] bg-white text-gray-600 dark:border-gray-700 dark:!bg-gray-900 dark:bg-inherit dark:text-inherit; + } + + /* Override prose-sm title sizes - 55% of original */ + .prose-sm :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + font-size: 1.17857em; /* 55% */ + @apply font-semibold; + } + + .prose-sm :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + font-size: 0.78571em; /* 55% */ + @apply font-semibold; + } + + .prose-sm :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + font-size: 0.70714em; /* 55% */ + @apply font-semibold; + } + + .prose-sm :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + font-size: 0.58929em; /* 55% */ + @apply font-semibold; + } + + .prose-sm :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + font-size: 0.55em; /* 55% */ + @apply font-semibold; + } + + .prose-sm :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) { + font-size: 0.55em; /* 55% */ + @apply font-semibold; + } +} + +.katex-display { + overflow: auto hidden; +} diff --git a/static/chatui/apple-touch-icon.png b/static/chatui/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..524518dd699807f430653f2976894df577a04afe Binary files /dev/null and b/static/chatui/apple-touch-icon.png differ diff --git a/static/chatui/favicon-dev.svg b/static/chatui/favicon-dev.svg new file mode 100644 index 0000000000000000000000000000000000000000..4d6dec1b0afa55f442126c579a683617f481da20 --- /dev/null +++ b/static/chatui/favicon-dev.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/chatui/favicon.ico b/static/chatui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7310d2fe68f8e6424bdb92850bbc269daa4e43e3 Binary files /dev/null and b/static/chatui/favicon.ico differ diff --git a/static/chatui/favicon.svg b/static/chatui/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..72d11947262a4e26c1852022148a3e70c18799a7 --- /dev/null +++ b/static/chatui/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/chatui/icon-128x128.png b/static/chatui/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..de9b83ab44749ea06341d580acb7c68d8aae8414 Binary files /dev/null and b/static/chatui/icon-128x128.png differ diff --git a/static/chatui/icon-144x144.png b/static/chatui/icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..af8c9fb4ef7decf4291a79227d1e89e75f0b5aac Binary files /dev/null and b/static/chatui/icon-144x144.png differ diff --git a/static/chatui/icon-192x192.png b/static/chatui/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..a2fba10cc08d1499129e9d209aba05d7345cfcf6 Binary files /dev/null and b/static/chatui/icon-192x192.png differ diff --git a/static/chatui/icon-256x256.png b/static/chatui/icon-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..e2190c37d708a47e31bd8f260f7deb57383d22a8 Binary files /dev/null and b/static/chatui/icon-256x256.png differ diff --git a/static/chatui/icon-36x36.png b/static/chatui/icon-36x36.png new file mode 100644 index 0000000000000000000000000000000000000000..6d861194085153d7ca9d2cacc9430cd27ee3fabe Binary files /dev/null and b/static/chatui/icon-36x36.png differ diff --git a/static/chatui/icon-48x48.png b/static/chatui/icon-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..117c6685d59c0d076c2d0e5dbb7dbe9727a9108c Binary files /dev/null and b/static/chatui/icon-48x48.png differ diff --git a/static/chatui/icon-512x512.png b/static/chatui/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..bb6cae1d1bb0c9cc5e223b437338129215b7c17c Binary files /dev/null and b/static/chatui/icon-512x512.png differ diff --git a/static/chatui/icon-72x72.png b/static/chatui/icon-72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..e1fa6ec4bbc233c86bf6b4c4ae1a4c30abd2d368 Binary files /dev/null and b/static/chatui/icon-72x72.png differ diff --git a/static/chatui/icon-96x96.png b/static/chatui/icon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..a12e25f22c81950c0c41581cee52758403202257 Binary files /dev/null and b/static/chatui/icon-96x96.png differ diff --git a/static/chatui/icon.svg b/static/chatui/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..72d11947262a4e26c1852022148a3e70c18799a7 --- /dev/null +++ b/static/chatui/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/chatui/logo.svg b/static/chatui/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..8fb1e74dc919175b14a07eb53f7ad2c8490db60f --- /dev/null +++ b/static/chatui/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/chatui/manifest.json b/static/chatui/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..4e4fa1dda4d589167054382333f537e10986690f --- /dev/null +++ b/static/chatui/manifest.json @@ -0,0 +1,54 @@ +{ + "background_color": "#ffffff", + "name": "ChatUI", + "short_name": "ChatUI", + "display": "standalone", + "start_url": "/chat", + "icons": [ + { + "src": "/chat/chatui/icon-36x36.png", + "sizes": "36x36", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/chat/chatui/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/static/huggingchat/apple-touch-icon.png b/static/huggingchat/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03c9beedf74681cd1698fe6669e53c9e3e76689f Binary files /dev/null and b/static/huggingchat/apple-touch-icon.png differ diff --git a/static/huggingchat/castle-example.jpg b/static/huggingchat/castle-example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5b932b33e0d3063ca2444e64164b4cdd9c6d6ad0 Binary files /dev/null and b/static/huggingchat/castle-example.jpg differ diff --git a/static/huggingchat/favicon-dark.svg b/static/huggingchat/favicon-dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..40817fe2a147f803b3fbd0403a2286a6fd540bf2 --- /dev/null +++ b/static/huggingchat/favicon-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/huggingchat/favicon-dev.svg b/static/huggingchat/favicon-dev.svg new file mode 100644 index 0000000000000000000000000000000000000000..242e31c41ab78d5c23fc6b9fdfc80f927543880a --- /dev/null +++ b/static/huggingchat/favicon-dev.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/huggingchat/favicon.ico b/static/huggingchat/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8360ec6177fd82c2b296a1e447df889fefd34ae2 Binary files /dev/null and b/static/huggingchat/favicon.ico differ diff --git a/static/huggingchat/favicon.svg b/static/huggingchat/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..f039d8ab37d53baf6b1bf6365e9291331a71a3dc --- /dev/null +++ b/static/huggingchat/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/huggingchat/fulltext-logo.svg b/static/huggingchat/fulltext-logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..e48aa869be93b971d2da65d99f246440f98dca90 --- /dev/null +++ b/static/huggingchat/fulltext-logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/static/huggingchat/icon-128x128.png b/static/huggingchat/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..dff051531aee638205dd261445582dcaf595e00e Binary files /dev/null and b/static/huggingchat/icon-128x128.png differ diff --git a/static/huggingchat/icon-144x144.png b/static/huggingchat/icon-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4d43b2cb998849877460d1c9cbaf65363aa1d2 Binary files /dev/null and b/static/huggingchat/icon-144x144.png differ diff --git a/static/huggingchat/icon-192x192.png b/static/huggingchat/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..6755df648c73d7164488e4afe1c87aacfc19d49c Binary files /dev/null and b/static/huggingchat/icon-192x192.png differ diff --git a/static/huggingchat/icon-256x256.png b/static/huggingchat/icon-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ef5f8b4afe2abb285bef8bee798b7d58b9dcaf Binary files /dev/null and b/static/huggingchat/icon-256x256.png differ diff --git a/static/huggingchat/icon-36x36.png b/static/huggingchat/icon-36x36.png new file mode 100644 index 0000000000000000000000000000000000000000..c54291b81ae4de5430aceec8741924c5767e8fec Binary files /dev/null and b/static/huggingchat/icon-36x36.png differ diff --git a/static/huggingchat/icon-48x48.png b/static/huggingchat/icon-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..c26df42ceb41317a0ab577b64ee5db0ca909f362 Binary files /dev/null and b/static/huggingchat/icon-48x48.png differ diff --git a/static/huggingchat/icon-512x512.png b/static/huggingchat/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..405ba4cc3181dd60594d7183cf192c963d2a22e5 Binary files /dev/null and b/static/huggingchat/icon-512x512.png differ diff --git a/static/huggingchat/icon-72x72.png b/static/huggingchat/icon-72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..fbf0e2023d6763ef7f17f8b6484f3d9662660e33 Binary files /dev/null and b/static/huggingchat/icon-72x72.png differ diff --git a/static/huggingchat/icon-96x96.png b/static/huggingchat/icon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..aaa27f7b19ec9a22d5987381cc34e044fea0d55e Binary files /dev/null and b/static/huggingchat/icon-96x96.png differ diff --git a/static/huggingchat/icon.svg b/static/huggingchat/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..65353d2b5d2c6fc7e6ae58c2aa8803c1edba5414 --- /dev/null +++ b/static/huggingchat/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/huggingchat/logo.svg b/static/huggingchat/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..c79e09a8f5d38fb23f9b8109b83a0d80cadc6464 --- /dev/null +++ b/static/huggingchat/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/huggingchat/manifest.json b/static/huggingchat/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..09888cf128babbc5d2831118b4615df9ede082ad --- /dev/null +++ b/static/huggingchat/manifest.json @@ -0,0 +1,54 @@ +{ + "background_color": "#ffffff", + "name": "HuggingChat", + "short_name": "HuggingChat", + "display": "standalone", + "start_url": "/chat", + "icons": [ + { + "src": "/chat/huggingchat/icon-36x36.png", + "sizes": "36x36", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/chat/huggingchat/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/static/huggingchat/routes.chat.json b/static/huggingchat/routes.chat.json new file mode 100644 index 0000000000000000000000000000000000000000..d4646cd94c04d14532b991c42c487566f3738bb9 --- /dev/null +++ b/static/huggingchat/routes.chat.json @@ -0,0 +1,226 @@ +[ + { + "name": "job_app_docs", + "description": "Create ATS‑ready resumes and cover letters aligned to a job posting.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": [ + "deepseek-ai/DeepSeek-V3.1", + "moonshotai/Kimi-K2-Instruct-0905", + "zai-org/GLM-4.6" + ] + }, + { + "name": "email_writing", + "description": "Draft or revise emails with clear tone and a specific CTA.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "google/gemma-3-27b-it"] + }, + { + "name": "social_media_copy", + "description": "Write platform‑specific social captions and short posts for engagement.", + "primary_model": "deepseek-ai/DeepSeek-V3.1", + "fallback_models": ["moonshotai/Kimi-K2-Instruct-0905", "Qwen/Qwen3-235B-A22B-Instruct-2507"] + }, + { + "name": "editing_rewrite", + "description": "Lightly proofread and rephrase text for tone, length, and clarity.", + "primary_model": "moonshotai/Kimi-K2-Instruct-0905", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "google/gemma-3-27b-it", "zai-org/GLM-4.6"] + }, + { + "name": "qa_explanations", + "description": "Provide concise answers and plain‑language explanations.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "meta-llama/Llama-3.3-70B-Instruct"] + }, + { + "name": "technical_explanation", + "description": "Explain complex technical topics step‑by‑step with worked examples.", + "primary_model": "deepseek-ai/DeepSeek-R1-0528", + "fallback_models": ["Qwen/QwQ-32B", "moonshotai/Kimi-K2-Instruct-0905"] + }, + { + "name": "essay_writing", + "description": "Plan and write essays from outline to draft; citations on request.", + "primary_model": "Qwen/Qwen3-235B-A22B-Thinking-2507", + "fallback_models": ["deepseek-ai/DeepSeek-R1-0528", "deepseek-ai/DeepSeek-V3.1"] + }, + { + "name": "summarization", + "description": "Condense documents into an abstract, key points, and action items.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": [ + "deepseek-ai/DeepSeek-V3.1", + "meta-llama/Llama-4-Maverick-17B-128E-Instruct" + ] + }, + { + "name": "translation", + "description": "Translate between languages with register and terminology control.", + "primary_model": "CohereLabs/command-a-translate-08-2025", + "fallback_models": ["CohereLabs/aya-expanse-32b", "google/gemma-3-27b-it"] + }, + { + "name": "language_tutoring", + "description": "Interactive language practice with conversation, grammar, vocab, and feedback.", + "primary_model": "CohereLabs/aya-expanse-32b", + "fallback_models": [ + "CohereLabs/aya-expanse-8b", + "google/gemma-3-27b-it", + "meta-llama/Llama-3.3-70B-Instruct" + ] + }, + { + "name": "formal_proof", + "description": "Produce Lean 4 proofs with tactic scripts and subgoals.", + "primary_model": "deepseek-ai/DeepSeek-Prover-V2-671B", + "fallback_models": ["deepseek-ai/DeepSeek-R1-0528", "Qwen/QwQ-32B"] + }, + { + "name": "software_architecture_design", + "description": "Design architectures: views, APIs, data models, and scalability trade‑offs.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "meta-llama/Llama-3.1-405B-Instruct"] + }, + { + "name": "agentic_orchestration", + "description": "Plan and execute tool/API calls with schemas, retries, and recovery.", + "primary_model": "openai/gpt-oss-120b", + "fallback_models": ["zai-org/GLM-4.6", "deepseek-ai/DeepSeek-V3.1"] + }, + { + "name": "code_generation", + "description": "Generate new code, tests, and scaffolds from specs.", + "primary_model": "Qwen/Qwen3-Coder-480B-A35B-Instruct", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "Qwen/Qwen3-Coder-30B-A3B-Instruct"] + }, + { + "name": "frontend_ui", + "description": "Build accessible, responsive UI components and pages.", + "primary_model": "deepseek-ai/DeepSeek-R1-0528", + "fallback_models": ["Qwen/Qwen3-Coder-480B-A35B-Instruct", "zai-org/GLM-4.6"] + }, + { + "name": "code_maintenance", + "description": "Fix bugs and refactor code; add tests.", + "primary_model": "Qwen/Qwen3-Coder-480B-A35B-Instruct", + "fallback_models": [ + "deepseek-ai/DeepSeek-V3.1", + "meta-llama/Llama-4-Maverick-17B-128E-Instruct" + ] + }, + { + "name": "code_review_docs", + "description": "Explain code and write docs, READMEs, and examples.", + "primary_model": "deepseek-ai/DeepSeek-V3.1", + "fallback_models": ["meta-llama/Llama-3.3-70B-Instruct", "Qwen/Qwen3-235B-A22B-Instruct-2507"] + }, + { + "name": "terminal_cli", + "description": "Solve Linux shell tasks with safe, idempotent commands.", + "primary_model": "zai-org/GLM-4.6", + "fallback_models": ["meta-llama/Llama-4-Maverick-17B-128E-Instruct", "Qwen/Qwen3-32B"] + }, + { + "name": "travel_planning", + "description": "Research trips and craft day‑by‑day itineraries with logistics.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": [ + "deepseek-ai/DeepSeek-V3.1", + "meta-llama/Llama-4-Maverick-17B-128E-Instruct" + ] + }, + { + "name": "shopping_recommendations", + "description": "Compare products and recommend ranked picks with rationale.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["zai-org/GLM-4.6", "deepseek-ai/DeepSeek-V3.1"] + }, + { + "name": "meal_planning", + "description": "Create meal plans and recipes by diet, budget, and time.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "google/gemma-3-27b-it"] + }, + { + "name": "decision_support", + "description": "Score options against criteria and recommend a choice.", + "primary_model": "deepseek-ai/DeepSeek-R1-0528", + "fallback_models": ["Qwen/Qwen3-235B-A22B-Thinking-2507", "deepseek-ai/DeepSeek-V3.1"] + }, + { + "name": "career_coaching", + "description": "Guide job search, skill gaps, interviews, and negotiation.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["meta-llama/Llama-3.3-70B-Instruct", "deepseek-ai/DeepSeek-V3.1"] + }, + { + "name": "personal_finance", + "description": "Build budgets, savings plans, and simple tracking schemas.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "Qwen/Qwen3-235B-A22B-Thinking-2507"] + }, + { + "name": "health_wellness_info", + "description": "Provide general health, fitness, sleep, and nutrition information.", + "primary_model": "aaditya/Llama3-OpenBioLLM-70B", + "fallback_models": ["Qwen/Qwen3-235B-A22B-Instruct-2507", "google/gemma-3-27b-it"] + }, + { + "name": "brainstorming_ideas", + "description": "Generate many creative ideas, then help narrow choices.", + "primary_model": "deepseek-ai/DeepSeek-V3.1", + "fallback_models": ["NousResearch/Hermes-4-70B", "Qwen/Qwen3-235B-A22B-Instruct-2507"] + }, + { + "name": "creative_writing", + "description": "Write fiction, poems, jokes, or scripts with style control.", + "primary_model": "moonshotai/Kimi-K2-Instruct-0905", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "meta-llama/Llama-3.3-70B-Instruct"] + }, + { + "name": "interactive_roleplay", + "description": "Run in‑character text adventures and persistent role‑play.", + "primary_model": "NousResearch/Hermes-4-70B", + "fallback_models": ["moonshotai/Kimi-K2-Instruct-0905", "Qwen/Qwen3-235B-A22B-Instruct-2507"] + }, + { + "name": "character_impersonation", + "description": "Act and imitate fictional character voices or invented personas consistently.", + "primary_model": "NousResearch/Hermes-4-70B", + "fallback_models": ["moonshotai/Kimi-K2-Instruct-0905", "Qwen/Qwen3-235B-A22B-Instruct-2507"] + }, + { + "name": "casual_conversation", + "description": "Engage in friendly and open‑ended casual chat.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": ["moonshotai/Kimi-K2-Instruct-0905", "google/gemma-3-27b-it"] + }, + { + "name": "emotional_support", + "description": "Provide compassionate listening and gentle guidance for emotional well-being.", + "primary_model": "Qwen/Qwen3-235B-A22B-Instruct-2507", + "fallback_models": [ + "meta-llama/Llama-4-Maverick-17B-128E-Instruct", + "deepseek-ai/DeepSeek-V3.1" + ] + }, + { + "name": "learning_tutor", + "description": "Teach concepts with step-by-step explanations, examples, and practice.", + "primary_model": "deepseek-ai/DeepSeek-V3.1", + "fallback_models": ["Qwen/Qwen3-235B-A22B-Thinking-2507", "deepseek-ai/DeepSeek-R1-0528"] + }, + { + "name": "structured_data", + "description": "Extract structured JSON from text.", + "primary_model": "zai-org/GLM-4.6", + "fallback_models": ["deepseek-ai/DeepSeek-V3.1", "Qwen/Qwen3-235B-A22B-Instruct-2507"] + }, + { + "name": "spell_checker", + "description": "Fix spelling, capitalization, punctuation, and obvious grammar errors.", + "primary_model": "CohereLabs/aya-expanse-32b", + "fallback_models": ["moonshotai/Kimi-K2-Instruct-0905", "google/gemma-3-27b-it"] + } +] diff --git a/static/huggingchat/thumbnail.png b/static/huggingchat/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..75c1f5f5d1966770a144b24a1f189663543a05dc Binary files /dev/null and b/static/huggingchat/thumbnail.png differ diff --git a/stub/@reflink/reflink/index.js b/stub/@reflink/reflink/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/stub/@reflink/reflink/package.json b/stub/@reflink/reflink/package.json new file mode 100644 index 0000000000000000000000000000000000000000..cf23252cff6ae5aa0572156f398217bdf983a9e3 --- /dev/null +++ b/stub/@reflink/reflink/package.json @@ -0,0 +1,5 @@ +{ + "name": "@reflink/reflink", + "version": "0.0.0", + "main": "index.js" +} diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000000000000000000000000000000000000..b4ebc14e503b02315dbfa24d37fcf6ac87fb592d --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,50 @@ +import adapter from "@sveltejs/adapter-node"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import dotenv from "dotenv"; +import { execSync } from "child_process"; + +dotenv.config({ path: "./.env.local", override: true }); +dotenv.config({ path: "./.env" }); + +function getCurrentCommitSHA() { + try { + return execSync("git rev-parse HEAD").toString(); + } catch (error) { + console.error("Error getting current commit SHA:", error); + return "unknown"; + } +} + +process.env.PUBLIC_VERSION ??= process.env.npm_package_version; +process.env.PUBLIC_COMMIT_SHA ??= getCurrentCommitSHA(); + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + adapter: adapter(), + + paths: { + base: process.env.APP_BASE || "", + relative: false, + }, + csrf: { + // handled in hooks.server.ts, because we can have multiple valid origins + checkOrigin: false, + }, + csp: { + directives: { + ...(process.env.ALLOW_IFRAME === "true" ? {} : { "frame-ancestors": ["'none'"] }), + }, + }, + alias: { + $api: "./src/lib/server/api", + "$api/*": "./src/lib/server/api/*", + }, + }, +}; + +export default config; diff --git a/tailwind.config.cjs b/tailwind.config.cjs new file mode 100644 index 0000000000000000000000000000000000000000..5779cfd64d18e20612d81aeae5a01a228a69c2d2 --- /dev/null +++ b/tailwind.config.cjs @@ -0,0 +1,30 @@ +const defaultTheme = require("tailwindcss/defaultTheme"); +const colors = require("tailwindcss/colors"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: "class", + mode: "jit", + content: ["./src/**/*.{html,js,svelte,ts}"], + theme: { + extend: { + colors: { + gray: { + 600: "#323843", + 700: "#252a33", + 800: "#1b1f27", + 900: "#12151c", + 950: "#07090d", + }, + }, + fontSize: { + xxs: "0.625rem", + smd: "0.94rem", + }, + }, + }, + plugins: [ + require("tailwind-scrollbar")({ nocompatible: true }), + require("@tailwindcss/typography"), + ], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..2e4b2d5d934e5919d887560fa339aac799ce36e4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2018" + }, + "exclude": ["vite.config.ts"] + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ffe92004665a4d2f52aeff583ecdf611612d157 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,81 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import Icons from "unplugin-icons/vite"; +import { promises } from "fs"; +import { defineConfig } from "vitest/config"; +import { config } from "dotenv"; + +config({ path: "./.env.local" }); + +// used to load fonts server side for thumbnail generation +function loadTTFAsArrayBuffer() { + return { + name: "load-ttf-as-array-buffer", + async transform(_src, id) { + if (id.endsWith(".ttf")) { + return `export default new Uint8Array([ + ${new Uint8Array(await promises.readFile(id))} + ]).buffer`; + } + }, + }; +} +export default defineConfig({ + plugins: [ + sveltekit(), + Icons({ + compiler: "svelte", + }), + loadTTFAsArrayBuffer(), + ], + // Allow external access via ngrok tunnel host + server: { + port: process.env.PORT ? parseInt(process.env.PORT) : 5173, + // Allow any ngrok-free.app subdomain (dynamic tunnels) + // See Vite server.allowedHosts: string[] | true + // Using leading dot matches subdomains per Vite's host check logic + allowedHosts: ["huggingface.ngrok.io"], + }, + optimizeDeps: { + include: ["uuid", "sharp", "clsx"], + }, + test: { + workspace: [ + { + // Client-side tests (Svelte components) + extends: "./vite.config.ts", + test: { + name: "client", + environment: "browser", + browser: { + enabled: true, + provider: "playwright", + instances: [{ browser: "chromium", headless: true }], + }, + include: ["src/**/*.svelte.{test,spec}.{js,ts}"], + exclude: ["src/lib/server/**", "src/**/*.ssr.{test,spec}.{js,ts}"], + setupFiles: ["./scripts/setups/vitest-setup-client.ts"], + }, + }, + { + // SSR tests (Server-side rendering) + extends: "./vite.config.ts", + test: { + name: "ssr", + environment: "node", + include: ["src/**/*.ssr.{test,spec}.{js,ts}"], + }, + }, + { + // Server-side tests (Node.js utilities) + extends: "./vite.config.ts", + test: { + name: "server", + environment: "node", + include: ["src/**/*.{test,spec}.{js,ts}"], + exclude: ["src/**/*.svelte.{test,spec}.{js,ts}", "src/**/*.ssr.{test,spec}.{js,ts}"], + setupFiles: ["./scripts/setups/vitest-setup-server.ts"], + }, + }, + ], + }, +});