From 96de2d25736d6c1a13ff56ab880b856b660921f0 Mon Sep 17 00:00:00 2001 From: mr-forust Date: Sun, 26 Jul 2026 19:39:09 +0200 Subject: [PATCH] feat(userbot): add Kubernetes control panel Manage Telegram instances through Kubernetes with legacy adoption for forust and anna. Build and deploy the panel image alongside the runtime. --- k8s/base/kustomization.yaml | 4 +- k8s/base/panel.yaml | 264 ++ k8s/base/userbots.yaml | 32 +- panel/.dockerignore | 7 + panel/.gitignore | 7 + panel/Dockerfile | 32 + panel/README.md | 43 + panel/backend/app/__init__.py | 1 + panel/backend/app/auth_service.py | 202 + panel/backend/app/config.py | 46 + panel/backend/app/errors.py | 5 + panel/backend/app/kubernetes_service.py | 522 +++ panel/backend/app/main.py | 198 + panel/backend/app/models.py | 89 + panel/backend/requirements-dev.txt | 4 + panel/backend/requirements.txt | 5 + panel/backend/tests/test_auth_service.py | 84 + .../backend/tests/test_kubernetes_service.py | 179 + panel/backend/tests/test_models.py | 44 + panel/frontend/index.html | 13 + panel/frontend/package-lock.json | 3970 +++++++++++++++++ panel/frontend/package.json | 26 + panel/frontend/src/App.svelte | 211 + .../src/components/AddAccountDrawer.svelte | 354 ++ .../src/components/DetailsDrawer.svelte | 204 + .../src/components/InstanceTable.svelte | 114 + panel/frontend/src/components/Sidebar.svelte | 39 + .../frontend/src/components/StatusDot.svelte | 17 + panel/frontend/src/lib/api.ts | 76 + panel/frontend/src/lib/instances.test.ts | 43 + panel/frontend/src/lib/instances.ts | 24 + panel/frontend/src/lib/types.ts | 45 + panel/frontend/src/main.ts | 8 + panel/frontend/src/styles.css | 1235 +++++ panel/frontend/src/test/setup.ts | 1 + panel/frontend/svelte.config.js | 5 + panel/frontend/tsconfig.json | 17 + panel/frontend/vite.config.ts | 15 + panel/frontend/vitest.config.ts | 8 + 39 files changed, 8188 insertions(+), 5 deletions(-) create mode 100644 k8s/base/panel.yaml create mode 100644 panel/.dockerignore create mode 100644 panel/.gitignore create mode 100644 panel/Dockerfile create mode 100644 panel/README.md create mode 100644 panel/backend/app/__init__.py create mode 100644 panel/backend/app/auth_service.py create mode 100644 panel/backend/app/config.py create mode 100644 panel/backend/app/errors.py create mode 100644 panel/backend/app/kubernetes_service.py create mode 100644 panel/backend/app/main.py create mode 100644 panel/backend/app/models.py create mode 100644 panel/backend/requirements-dev.txt create mode 100644 panel/backend/requirements.txt create mode 100644 panel/backend/tests/test_auth_service.py create mode 100644 panel/backend/tests/test_kubernetes_service.py create mode 100644 panel/backend/tests/test_models.py create mode 100644 panel/frontend/index.html create mode 100644 panel/frontend/package-lock.json create mode 100644 panel/frontend/package.json create mode 100644 panel/frontend/src/App.svelte create mode 100644 panel/frontend/src/components/AddAccountDrawer.svelte create mode 100644 panel/frontend/src/components/DetailsDrawer.svelte create mode 100644 panel/frontend/src/components/InstanceTable.svelte create mode 100644 panel/frontend/src/components/Sidebar.svelte create mode 100644 panel/frontend/src/components/StatusDot.svelte create mode 100644 panel/frontend/src/lib/api.ts create mode 100644 panel/frontend/src/lib/instances.test.ts create mode 100644 panel/frontend/src/lib/instances.ts create mode 100644 panel/frontend/src/lib/types.ts create mode 100644 panel/frontend/src/main.ts create mode 100644 panel/frontend/src/styles.css create mode 100644 panel/frontend/src/test/setup.ts create mode 100644 panel/frontend/svelte.config.js create mode 100644 panel/frontend/tsconfig.json create mode 100644 panel/frontend/vite.config.ts create mode 100644 panel/frontend/vitest.config.ts diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml index ea5fa72..b0a67de 100644 --- a/k8s/base/kustomization.yaml +++ b/k8s/base/kustomization.yaml @@ -3,7 +3,5 @@ kind: Kustomization resources: - userbots.yaml - - common-secret.yaml + - panel.yaml - common-config.yaml - - forust-secrets.yaml - - anna-secrets.yaml diff --git a/k8s/base/panel.yaml b/k8s/base/panel.yaml new file mode 100644 index 0000000..4f738c8 --- /dev/null +++ b/k8s/base/panel.yaml @@ -0,0 +1,264 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: userbot +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: userbot-panel + namespace: userbot +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: userbot-runtime + namespace: userbot +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: userbot-common-config + namespace: userbot +data: + DATABASE_TYPE: "" + DATABASE_NAME: "" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: userbot-panel + namespace: userbot +rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch + - create + - patch + - delete + - apiGroups: + - apps + resources: + - deployments/scale + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods/log + verbs: + - get + - apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - watch + - create + - delete + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - create + - delete + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - apiGroups: + - metrics.k8s.io + resources: + - pods + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: userbot-panel + namespace: userbot +subjects: + - kind: ServiceAccount + name: userbot-panel + namespace: userbot +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: userbot-panel +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: userbot-panel-legacy + namespace: default +rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch + - patch + - apiGroups: + - apps + resources: + - deployments/scale + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods/log + verbs: + - get + - apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - apiGroups: + - metrics.k8s.io + resources: + - pods + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: userbot-panel-legacy + namespace: default +subjects: + - kind: ServiceAccount + name: userbot-panel + namespace: userbot +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: userbot-panel-legacy +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: userbot-panel + namespace: userbot + labels: + app: userbot-panel +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: userbot-panel + template: + metadata: + labels: + app: userbot-panel + spec: + serviceAccountName: userbot-panel + containers: + - name: userbot-panel + image: gcr.forust.xyz/forust/userbot-panel:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 8080 + env: + - name: USERBOT_NAMESPACE + value: userbot + - name: USERBOT_LEGACY_NAMESPACES + value: default + - name: USERBOT_IMAGE + value: gcr.forust.xyz/forust/userbot:latest + - name: USERBOT_STORAGE_CLASS + value: local-path-retain + - name: USERBOT_DOWNLOADS_HOST_PATH + value: /srv/homelab/userbot/Downloads + resources: + requests: + cpu: 20m + memory: 96Mi + limits: + cpu: 300m + memory: 384Mi + readinessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 3 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 10 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: userbot-panel + namespace: userbot +spec: + type: ClusterIP + selector: + app: userbot-panel + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: userbot-panel-route + namespace: userbot +spec: + entryPoints: + - websecure + routes: + - match: Host(`userbot.workstation.internal`) + kind: Rule + services: + - name: userbot-panel + port: 8080 diff --git a/k8s/base/userbots.yaml b/k8s/base/userbots.yaml index 74618d9..95664c0 100644 --- a/k8s/base/userbots.yaml +++ b/k8s/base/userbots.yaml @@ -4,8 +4,14 @@ metadata: name: forust-userbot-deployment labels: app: forust-userbot + app.kubernetes.io/name: userbot + app.kubernetes.io/instance: forust + annotations: + userbot.forust.xyz/display-name: forust + userbot.forust.xyz/legacy: "true" + userbot.forust.xyz/credentials-secret: userbot-forust-secrets + userbot.forust.xyz/pvc: forust-pvc spec: - replicas: 1 selector: matchLabels: app: forust-userbot @@ -13,6 +19,8 @@ spec: metadata: labels: app: forust-userbot + annotations: + kubectl.kubernetes.io/restartedAt: "2026-07-25T20:24:51+02:00" spec: containers: - name: forust-userbot @@ -33,6 +41,8 @@ spec: - secretRef: name: userbot-forust-secrets volumeMounts: + - name: downloads + mountPath: /app/downloads - name: forust-storage mountPath: /app/data subPath: data @@ -40,6 +50,10 @@ spec: mountPath: /app/logs subPath: logs volumes: + - name: downloads + hostPath: + path: /srv/homelab/userbot/Downloads + type: DirectoryOrCreate - name: forust-storage persistentVolumeClaim: claimName: forust-pvc @@ -62,8 +76,14 @@ metadata: name: anna-userbot-deployment labels: app: anna-userbot + app.kubernetes.io/name: userbot + app.kubernetes.io/instance: anna + annotations: + userbot.forust.xyz/display-name: anna + userbot.forust.xyz/legacy: "true" + userbot.forust.xyz/credentials-secret: userbot-anna-secrets + userbot.forust.xyz/pvc: anna-pvc spec: - replicas: 1 selector: matchLabels: app: anna-userbot @@ -71,6 +91,8 @@ spec: metadata: labels: app: anna-userbot + annotations: + kubectl.kubernetes.io/restartedAt: "2026-07-25T20:24:51+02:00" spec: containers: - name: anna-userbot @@ -91,6 +113,8 @@ spec: - secretRef: name: userbot-anna-secrets volumeMounts: + - name: downloads + mountPath: /app/downloads - name: anna-storage mountPath: /app/data subPath: data @@ -98,6 +122,10 @@ spec: mountPath: /app/logs subPath: logs volumes: + - name: downloads + hostPath: + path: /srv/homelab/userbot/Downloads + type: DirectoryOrCreate - name: anna-storage persistentVolumeClaim: claimName: anna-pvc diff --git a/panel/.dockerignore b/panel/.dockerignore new file mode 100644 index 0000000..225aa88 --- /dev/null +++ b/panel/.dockerignore @@ -0,0 +1,7 @@ +backend/.venv +frontend/node_modules +frontend/dist +__pycache__ +.pytest_cache +.ruff_cache +*.pyc diff --git a/panel/.gitignore b/panel/.gitignore new file mode 100644 index 0000000..9674ad6 --- /dev/null +++ b/panel/.gitignore @@ -0,0 +1,7 @@ +backend/.venv/ +frontend/node_modules/ +frontend/dist/ +.pytest_cache/ +.ruff_cache/ +__pycache__/ +*.pyc diff --git a/panel/Dockerfile b/panel/Dockerfile new file mode 100644 index 0000000..ba66193 --- /dev/null +++ b/panel/Dockerfile @@ -0,0 +1,32 @@ +FROM node:22-alpine AS frontend + +WORKDIR /src +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +FROM python:3.13-slim AS python-builder + +WORKDIR /build +COPY backend/requirements.txt ./ +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && pip install --no-cache-dir --prefix=/install -r requirements.txt \ + && rm -rf /var/lib/apt/lists/* + +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PANEL_STATIC_DIR=/app/static + +WORKDIR /app +COPY --from=python-builder /install /usr/local +COPY backend/app ./app +COPY --from=frontend /src/dist ./static + +USER 65532:65532 +EXPOSE 8080 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/panel/README.md b/panel/README.md new file mode 100644 index 0000000..f34349c --- /dev/null +++ b/panel/README.md @@ -0,0 +1,43 @@ +# Userbot Control Panel + +Локальная панель управляет userbot-инстансами напрямую через Kubernetes API. +Новые аккаунты создаются в namespace `userbot`; существующие `forust` и `anna` +остаются в `default` и принимаются под управление только через metadata labels. + +## Первый запуск + +Скопировать общий Secret в новый namespace, не печатая его значения: + +```bash +kubectl create namespace userbot --dry-run=client -o yaml | kubectl apply -f - +kubectl get secret userbot-common-secrets -n default -o json \ + | jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \ + | kubectl apply -f - +``` + +Затем применить манифесты: + +```bash +kubectl apply -f userbot/k8s/base/panel.yaml +kubectl apply -f userbot/k8s/base/userbots.yaml +kubectl rollout status deployment/userbot-panel -n userbot +``` + +Панель доступна только через внутренний Traefik route: +`https://userbot.workstation.internal`. + +## Локальная разработка + +```bash +cd userbot/panel/frontend +npm install +npm run dev +``` + +Backend использует in-cluster config, а вне кластера — текущий kubeconfig: + +```bash +cd userbot/panel/backend +uv run --with-requirements requirements-dev.txt \ + uvicorn app.main:app --reload --port 8080 +``` diff --git a/panel/backend/app/__init__.py b/panel/backend/app/__init__.py new file mode 100644 index 0000000..8fed5c4 --- /dev/null +++ b/panel/backend/app/__init__.py @@ -0,0 +1 @@ +"""Userbot Kubernetes control panel backend.""" diff --git a/panel/backend/app/auth_service.py b/panel/backend/app/auth_service.py new file mode 100644 index 0000000..b820c0f --- /dev/null +++ b/panel/backend/app/auth_service.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +import asyncio +import secrets +from contextlib import suppress +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta + +from pyrogram import Client +from pyrogram.errors import ( + ApiIdInvalid, + FloodWait, + PasswordHashInvalid, + PhoneCodeExpired, + PhoneCodeInvalid, + PhoneNumberInvalid, + RPCError, + SessionPasswordNeeded, + Unauthorized, +) + +from .errors import PanelError +from .models import AccountBase, PhoneStart, StringSessionStart + + +@dataclass +class AuthFlow: + flow_id: str + account: PhoneStart + client: Client + phone_code_hash: str + expires_at: datetime + + +@dataclass +class AuthorizedAccount: + account: AccountBase + session_string: str + + +class TelegramAuthService: + def __init__(self, ttl_seconds: int = 600) -> None: + self.ttl = timedelta(seconds=ttl_seconds) + self.flows: dict[str, AuthFlow] = {} + self._lock = asyncio.Lock() + + async def start_phone(self, account: PhoneStart) -> str: + await self._cleanup_expired() + flow_id = secrets.token_urlsafe(24) + telegram = Client( + f"auth-{flow_id}", + api_id=account.api_id, + api_hash=account.api_hash, + in_memory=True, + no_updates=True, + ) + try: + await telegram.connect() + sent = await telegram.send_code(account.phone) + except Exception as exc: + await self._disconnect(telegram) + raise self._translate(exc) from exc + + flow = AuthFlow( + flow_id=flow_id, + account=account, + client=telegram, + phone_code_hash=sent.phone_code_hash, + expires_at=datetime.now(UTC) + self.ttl, + ) + async with self._lock: + self.flows[flow_id] = flow + return flow_id + + async def submit_code( + self, + flow_id: str, + code: str, + ) -> AuthorizedAccount | None: + flow = await self._get(flow_id) + try: + await flow.client.sign_in( + flow.account.phone, + flow.phone_code_hash, + code, + ) + except SessionPasswordNeeded: + return None + except PhoneCodeExpired as exc: + await self._discard(flow) + raise self._translate(exc) from exc + except Exception as exc: + raise self._translate(exc) from exc + return await self._finish(flow) + + async def submit_password( + self, + flow_id: str, + password: str, + ) -> AuthorizedAccount: + flow = await self._get(flow_id) + try: + await flow.client.check_password(password) + except Exception as exc: + raise self._translate(exc) from exc + return await self._finish(flow) + + async def validate_string_session( + self, + account: StringSessionStart, + ) -> AuthorizedAccount: + telegram = Client( + f"validate-{secrets.token_urlsafe(12)}", + api_id=account.api_id, + api_hash=account.api_hash, + session_string=account.session_string, + in_memory=True, + no_updates=True, + ) + try: + await telegram.connect() + await telegram.get_me() + session_string = await telegram.export_session_string() + except Exception as exc: + raise self._translate(exc, session=True) from exc + finally: + await self._disconnect(telegram) + return AuthorizedAccount(account=account, session_string=session_string) + + async def close(self) -> None: + async with self._lock: + flows = list(self.flows.values()) + self.flows.clear() + await asyncio.gather( + *(self._disconnect(flow.client) for flow in flows), + return_exceptions=True, + ) + + async def _get(self, flow_id: str) -> AuthFlow: + await self._cleanup_expired() + async with self._lock: + flow = self.flows.get(flow_id) + if flow is None: + raise PanelError(410, "Authorization flow expired; start again") + return flow + + async def _finish(self, flow: AuthFlow) -> AuthorizedAccount: + try: + await flow.client.get_me() + session_string = await flow.client.export_session_string() + except Exception as exc: + raise self._translate(exc) from exc + finally: + async with self._lock: + self.flows.pop(flow.flow_id, None) + await self._disconnect(flow.client) + return AuthorizedAccount(account=flow.account, session_string=session_string) + + async def _discard(self, flow: AuthFlow) -> None: + async with self._lock: + self.flows.pop(flow.flow_id, None) + await self._disconnect(flow.client) + + async def _cleanup_expired(self) -> None: + now = datetime.now(UTC) + async with self._lock: + expired = [ + self.flows.pop(flow_id) + for flow_id, flow in list(self.flows.items()) + if flow.expires_at <= now + ] + if expired: + await asyncio.gather( + *(self._disconnect(flow.client) for flow in expired), + return_exceptions=True, + ) + + @staticmethod + async def _disconnect(telegram: Client) -> None: + with suppress(Exception): + if telegram.is_connected: + await telegram.disconnect() + + @staticmethod + def _translate(exc: Exception, *, session: bool = False) -> PanelError: + if isinstance(exc, FloodWait): + return PanelError(429, f"Telegram rate limit; retry in {exc.value} seconds") + if isinstance(exc, ApiIdInvalid): + return PanelError(422, "Telegram API ID or API Hash is invalid") + if isinstance(exc, PhoneNumberInvalid): + return PanelError(422, "Phone number is invalid") + if isinstance(exc, PhoneCodeInvalid): + return PanelError(422, "Telegram code is invalid") + if isinstance(exc, PhoneCodeExpired): + return PanelError(410, "Telegram code expired; start again") + if isinstance(exc, PasswordHashInvalid): + return PanelError(422, "2FA password is invalid") + if session and isinstance(exc, (Unauthorized, RPCError)): + return PanelError(422, "StringSession is invalid or expired") + if isinstance(exc, RPCError): + return PanelError(422, "Telegram rejected the authorization request") + return PanelError(503, "Telegram authorization is unavailable") diff --git a/panel/backend/app/config.py b/panel/backend/app/config.py new file mode 100644 index 0000000..c0b42b2 --- /dev/null +++ b/panel/backend/app/config.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class Settings: + namespace: str = os.environ.get("USERBOT_NAMESPACE", "userbot") + legacy_namespaces: tuple[str, ...] = tuple( + value.strip() + for value in os.environ.get("USERBOT_LEGACY_NAMESPACES", "default").split(",") + if value.strip() + ) + image: str = os.environ.get( + "USERBOT_IMAGE", + "gcr.forust.xyz/forust/userbot:latest", + ) + common_secret: str = os.environ.get( + "USERBOT_COMMON_SECRET", + "userbot-common-secrets", + ) + common_config: str = os.environ.get( + "USERBOT_COMMON_CONFIG", + "userbot-common-config", + ) + storage_class: str = os.environ.get( + "USERBOT_STORAGE_CLASS", + "local-path-retain", + ) + downloads_host_path: str = os.environ.get( + "USERBOT_DOWNLOADS_HOST_PATH", + "/srv/homelab/userbot/Downloads", + ) + static_dir: Path = Path(os.environ.get("PANEL_STATIC_DIR", "/app/static")) + auth_ttl_seconds: int = int(os.environ.get("PANEL_AUTH_TTL_SECONDS", "600")) + default_storage: str = os.environ.get("USERBOT_DEFAULT_STORAGE", "1Gi") + default_cpu_limit: str = os.environ.get("USERBOT_DEFAULT_CPU_LIMIT", "300m") + default_memory_limit: str = os.environ.get( + "USERBOT_DEFAULT_MEMORY_LIMIT", + "1536Mi", + ) + + +settings = Settings() diff --git a/panel/backend/app/errors.py b/panel/backend/app/errors.py new file mode 100644 index 0000000..0a579ca --- /dev/null +++ b/panel/backend/app/errors.py @@ -0,0 +1,5 @@ +class PanelError(Exception): + def __init__(self, status_code: int, detail: str) -> None: + super().__init__(detail) + self.status_code = status_code + self.detail = detail diff --git a/panel/backend/app/kubernetes_service.py b/panel/backend/app/kubernetes_service.py new file mode 100644 index 0000000..fde97d2 --- /dev/null +++ b/panel/backend/app/kubernetes_service.py @@ -0,0 +1,522 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from kubernetes import client, config +from kubernetes.client.exceptions import ApiException + +from .config import Settings +from .errors import PanelError +from .models import AccountBase, InstanceSummary + +MANAGED_LABEL = "app.kubernetes.io/name=userbot" +INSTANCE_LABEL = "app.kubernetes.io/instance" +MANAGED_BY_LABEL = "app.kubernetes.io/managed-by" +DISPLAY_ANNOTATION = "userbot.forust.xyz/display-name" +LEGACY_ANNOTATION = "userbot.forust.xyz/legacy" +CREDENTIALS_ANNOTATION = "userbot.forust.xyz/credentials-secret" +PVC_ANNOTATION = "userbot.forust.xyz/pvc" +RESTART_ANNOTATION = "userbot.forust.xyz/restarted-at" + + +def _selector(labels: dict[str, str] | None) -> str: + return ",".join(f"{key}={value}" for key, value in (labels or {}).items()) + + +def _as_datetime(value: Any) -> datetime | None: + if value is None: + return None + if isinstance(value, datetime): + return value + return getattr(value, "replace", lambda **_: None)(tzinfo=UTC) + + +class KubernetesService: + def __init__( + self, + settings: Settings, + *, + core: client.CoreV1Api | None = None, + apps: client.AppsV1Api | None = None, + custom: client.CustomObjectsApi | None = None, + ) -> None: + self.settings = settings + if core is None or apps is None: + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + self.core = core or client.CoreV1Api() + self.apps = apps or client.AppsV1Api() + self.custom = custom or client.CustomObjectsApi() + + def ensure_prerequisites(self) -> None: + try: + self.core.read_namespaced_secret( + self.settings.common_secret, + self.settings.namespace, + ) + self.core.read_namespaced_config_map( + self.settings.common_config, + self.settings.namespace, + ) + except ApiException as exc: + if exc.status == 404: + raise PanelError( + 503, + "Userbot common Secret or ConfigMap is missing in the userbot namespace", + ) from exc + raise self._api_error(exc, "Could not verify userbot prerequisites") from exc + + def list_instances( + self, + query: str = "", + status: str = "", + ) -> list[InstanceSummary]: + instances: list[InstanceSummary] = [] + for namespace in (self.settings.namespace, *self.settings.legacy_namespaces): + try: + deployments = self.apps.list_namespaced_deployment( + namespace, + label_selector=MANAGED_LABEL, + ).items + except ApiException as exc: + raise self._api_error(exc, f"Could not list Deployments in {namespace}") from exc + instances.extend(self._summarize(namespace, deployment) for deployment in deployments) + + query = query.strip().lower() + if query: + instances = [ + item + for item in instances + if query in item.instance_id.lower() + or query in item.display_name.lower() + or query in (item.pod or "").lower() + ] + if status: + instances = [item for item in instances if item.status == status] + return sorted(instances, key=lambda item: (item.legacy, item.instance_id)) + + def get_instance(self, instance_id: str) -> InstanceSummary: + namespace, deployment = self._find_deployment(instance_id) + return self._summarize(namespace, deployment) + + def assert_available(self, instance_id: str) -> None: + names = self._resource_names(instance_id) + checks = ( + (self.apps.read_namespaced_deployment, names["deployment"], "Deployment"), + (self.core.read_namespaced_secret, names["secret"], "Secret"), + (self.core.read_namespaced_persistent_volume_claim, names["pvc"], "PVC"), + ) + for read, name, kind in checks: + try: + read(name, self.settings.namespace) + except ApiException as exc: + if exc.status == 404: + continue + raise self._api_error(exc, f"Could not check {kind} {name}") from exc + raise PanelError(409, f"{kind} {name} already exists") + + def provision(self, account: AccountBase, session_string: str) -> InstanceSummary: + self.ensure_prerequisites() + self.assert_available(account.instance_id) + names = self._resource_names(account.instance_id) + created: list[tuple[str, str]] = [] + try: + self.core.create_namespaced_secret( + self.settings.namespace, + self._secret(account, session_string, names), + ) + created.append(("secret", names["secret"])) + self.core.create_namespaced_persistent_volume_claim( + self.settings.namespace, + self._pvc(account, names), + ) + created.append(("pvc", names["pvc"])) + self.apps.create_namespaced_deployment( + self.settings.namespace, + self._deployment(account, names), + ) + created.append(("deployment", names["deployment"])) + except ApiException as exc: + self._rollback(created) + raise self._api_error(exc, "Could not create userbot instance") from exc + return self.get_instance(account.instance_id) + + def scale(self, instance_id: str, replicas: int) -> InstanceSummary: + namespace, deployment = self._find_deployment(instance_id) + try: + self.apps.patch_namespaced_deployment_scale( + deployment.metadata.name, + namespace, + {"spec": {"replicas": replicas}}, + ) + except ApiException as exc: + raise self._api_error(exc, "Could not scale userbot instance") from exc + return self.get_instance(instance_id) + + def restart(self, instance_id: str) -> InstanceSummary: + namespace, deployment = self._find_deployment(instance_id) + if (deployment.spec.replicas or 0) == 0: + raise PanelError(409, "Stopped instance cannot be restarted") + timestamp = datetime.now(UTC).isoformat() + try: + self.apps.patch_namespaced_deployment( + deployment.metadata.name, + namespace, + { + "spec": { + "template": { + "metadata": { + "annotations": {RESTART_ANNOTATION: timestamp}, + } + } + } + }, + ) + except ApiException as exc: + raise self._api_error(exc, "Could not restart userbot instance") from exc + return self.get_instance(instance_id) + + def logs(self, instance_id: str, tail: int = 250) -> str: + namespace, deployment = self._find_deployment(instance_id) + pods = self._pods_for_deployment(namespace, deployment) + if not pods: + raise PanelError(409, "Userbot Pod is not running") + pod = pods[0] + container = deployment.spec.template.spec.containers[0].name + try: + return self.core.read_namespaced_pod_log( + pod.metadata.name, + namespace, + container=container, + tail_lines=max(1, min(tail, 1000)), + timestamps=True, + ) + except ApiException as exc: + raise self._api_error(exc, "Could not read userbot logs") from exc + + def delete(self, instance_id: str, *, delete_data: bool) -> None: + namespace, deployment = self._find_deployment(instance_id) + annotations = deployment.metadata.annotations or {} + if annotations.get(LEGACY_ANNOTATION) == "true" or namespace != self.settings.namespace: + raise PanelError(409, "Legacy instances cannot be deleted from the panel") + + names = self._resource_names(instance_id) + secret_name = annotations.get(CREDENTIALS_ANNOTATION, names["secret"]) + pvc_name = annotations.get(PVC_ANNOTATION, names["pvc"]) + operations = [ + ( + self.apps.delete_namespaced_deployment, + (deployment.metadata.name, namespace), + {"propagation_policy": "Foreground"}, + ), + (self.core.delete_namespaced_secret, (secret_name, namespace), {}), + ] + if delete_data: + operations.append( + ( + self.core.delete_namespaced_persistent_volume_claim, + (pvc_name, namespace), + {}, + ) + ) + for delete_resource, args, kwargs in operations: + try: + delete_resource(*args, **kwargs) + except ApiException as exc: + if exc.status != 404: + raise self._api_error(exc, "Could not delete userbot instance") from exc + + def _find_deployment(self, instance_id: str) -> tuple[str, Any]: + selector = f"{MANAGED_LABEL},{INSTANCE_LABEL}={instance_id}" + for namespace in (self.settings.namespace, *self.settings.legacy_namespaces): + try: + items = self.apps.list_namespaced_deployment( + namespace, + label_selector=selector, + ).items + except ApiException as exc: + raise self._api_error(exc, "Could not find userbot instance") from exc + if items: + return namespace, items[0] + raise PanelError(404, f"Instance {instance_id} does not exist") + + def _summarize(self, namespace: str, deployment: Any) -> InstanceSummary: + labels = deployment.metadata.labels or {} + annotations = deployment.metadata.annotations or {} + instance_id = labels.get(INSTANCE_LABEL, deployment.metadata.name) + legacy = annotations.get(LEGACY_ANNOTATION) == "true" + pods = self._pods_for_deployment(namespace, deployment) + pod = pods[0] if pods else None + desired = deployment.spec.replicas or 0 + ready = False + reason: str | None = None + restarts = 0 + updated_at = deployment.metadata.creation_timestamp + + if pod is not None: + statuses = pod.status.container_statuses or [] + ready = bool(statuses) and all(item.ready for item in statuses) + restarts = sum(item.restart_count for item in statuses) + for item in statuses: + state = item.state + if state and state.waiting and state.waiting.reason: + reason = state.waiting.reason + break + if state and state.terminated and state.terminated.reason: + reason = state.terminated.reason + break + updated_at = pod.status.start_time or pod.metadata.creation_timestamp + + if desired == 0: + status = "stopped" + elif reason in { + "CrashLoopBackOff", + "Error", + "ImagePullBackOff", + "ErrImagePull", + "CreateContainerConfigError", + "RunContainerError", + } or (pod is not None and pod.status.phase == "Failed"): + status = "error" + elif ready and (deployment.status.available_replicas or 0) > 0: + status = "running" + else: + status = "pending" + reason = reason or (pod.status.phase if pod is not None else "Scheduling") + + container_spec = deployment.spec.template.spec.containers[0] + limits = (container_spec.resources.limits or {}) if container_spec.resources else {} + pvc_name = annotations.get(PVC_ANNOTATION) or self._deployment_pvc(deployment) + storage = self._pvc_storage(namespace, pvc_name) + cpu_usage, memory_usage = self._pod_metrics(namespace, pod.metadata.name if pod else None) + return InstanceSummary( + instance_id=instance_id, + display_name=annotations.get(DISPLAY_ANNOTATION, instance_id), + namespace=namespace, + deployment=deployment.metadata.name, + pod=pod.metadata.name if pod else None, + status=status, + reason=reason, + ready=ready, + restarts=restarts, + pvc=pvc_name, + storage=storage, + image=container_spec.image, + cpu_limit=limits.get("cpu"), + memory_limit=limits.get("memory"), + cpu_usage=cpu_usage, + memory_usage=memory_usage, + updated_at=_as_datetime(updated_at), + legacy=legacy, + deletable=not legacy and namespace == self.settings.namespace, + ) + + def _pods_for_deployment(self, namespace: str, deployment: Any) -> list[Any]: + selector = _selector(deployment.spec.selector.match_labels) + try: + pods = self.core.list_namespaced_pod( + namespace, + label_selector=selector, + ).items + except ApiException as exc: + raise self._api_error(exc, "Could not list userbot Pods") from exc + return sorted( + pods, + key=lambda pod: pod.metadata.creation_timestamp or datetime.min.replace(tzinfo=UTC), + reverse=True, + ) + + def _pod_metrics(self, namespace: str, pod_name: str | None) -> tuple[str | None, str | None]: + if not pod_name: + return None, None + try: + metrics = self.custom.get_namespaced_custom_object( + "metrics.k8s.io", + "v1beta1", + namespace, + "pods", + pod_name, + ) + except (ApiException, AttributeError): + return None, None + containers = metrics.get("containers", []) + cpu = containers[0].get("usage", {}).get("cpu") if containers else None + memory = containers[0].get("usage", {}).get("memory") if containers else None + return cpu, memory + + def _pvc_storage(self, namespace: str, pvc_name: str | None) -> str | None: + if not pvc_name: + return None + try: + pvc = self.core.read_namespaced_persistent_volume_claim(pvc_name, namespace) + except ApiException: + return None + requests = pvc.spec.resources.requests or {} + return requests.get("storage") + + @staticmethod + def _deployment_pvc(deployment: Any) -> str | None: + for volume in deployment.spec.template.spec.volumes or []: + if volume.persistent_volume_claim: + return volume.persistent_volume_claim.claim_name + return None + + @staticmethod + def _resource_names(instance_id: str) -> dict[str, str]: + return { + "deployment": f"userbot-{instance_id}", + "secret": f"userbot-{instance_id}-credentials", + "pvc": f"userbot-{instance_id}-data", + } + + def _metadata( + self, + account: AccountBase, + names: dict[str, str], + resource_name: str, + ) -> client.V1ObjectMeta: + return client.V1ObjectMeta( + name=resource_name, + namespace=self.settings.namespace, + labels={ + "app.kubernetes.io/name": "userbot", + INSTANCE_LABEL: account.instance_id, + MANAGED_BY_LABEL: "userbot-panel", + }, + annotations={ + DISPLAY_ANNOTATION: account.display_name, + CREDENTIALS_ANNOTATION: names["secret"], + PVC_ANNOTATION: names["pvc"], + }, + ) + + def _secret( + self, + account: AccountBase, + session_string: str, + names: dict[str, str], + ) -> client.V1Secret: + return client.V1Secret( + metadata=self._metadata(account, names, names["secret"]), + type="Opaque", + string_data={ + "API_ID": str(account.api_id), + "API_HASH": account.api_hash, + "STRINGSESSION": session_string, + }, + ) + + def _pvc( + self, + account: AccountBase, + names: dict[str, str], + ) -> client.V1PersistentVolumeClaim: + return client.V1PersistentVolumeClaim( + metadata=self._metadata(account, names, names["pvc"]), + spec=client.V1PersistentVolumeClaimSpec( + access_modes=["ReadWriteOnce"], + storage_class_name=self.settings.storage_class, + resources=client.V1VolumeResourceRequirements( + requests={"storage": account.resources.storage}, + ), + ), + ) + + def _deployment( + self, + account: AccountBase, + names: dict[str, str], + ) -> client.V1Deployment: + pod_labels = { + "app.kubernetes.io/name": "userbot", + INSTANCE_LABEL: account.instance_id, + MANAGED_BY_LABEL: "userbot-panel", + } + container = client.V1Container( + name="userbot", + image=self.settings.image, + image_pull_policy="Always", + env_from=[ + client.V1EnvFromSource( + secret_ref=client.V1SecretEnvSource(name=self.settings.common_secret) + ), + client.V1EnvFromSource( + config_map_ref=client.V1ConfigMapEnvSource(name=self.settings.common_config) + ), + client.V1EnvFromSource( + secret_ref=client.V1SecretEnvSource(name=names["secret"]) + ), + ], + resources=client.V1ResourceRequirements( + requests={"cpu": "80m", "memory": "512Mi"}, + limits={ + "cpu": account.resources.cpu_limit, + "memory": account.resources.memory_limit, + }, + ), + volume_mounts=[ + client.V1VolumeMount(name="data", mount_path="/app/data"), + client.V1VolumeMount(name="downloads", mount_path="/app/downloads"), + ], + ) + pod_spec = client.V1PodSpec( + service_account_name="userbot-runtime", + automount_service_account_token=False, + containers=[container], + termination_grace_period_seconds=30, + volumes=[ + client.V1Volume( + name="data", + persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource( + claim_name=names["pvc"] + ), + ), + client.V1Volume( + name="downloads", + host_path=client.V1HostPathVolumeSource( + path=self.settings.downloads_host_path, + type="DirectoryOrCreate", + ), + ), + ], + ) + return client.V1Deployment( + metadata=self._metadata(account, names, names["deployment"]), + spec=client.V1DeploymentSpec( + replicas=1, + strategy=client.V1DeploymentStrategy(type="Recreate"), + selector=client.V1LabelSelector(match_labels=pod_labels), + template=client.V1PodTemplateSpec( + metadata=client.V1ObjectMeta(labels=pod_labels), + spec=pod_spec, + ), + ), + ) + + def _rollback(self, created: list[tuple[str, str]]) -> None: + for kind, name in reversed(created): + try: + if kind == "deployment": + self.apps.delete_namespaced_deployment(name, self.settings.namespace) + elif kind == "pvc": + self.core.delete_namespaced_persistent_volume_claim( + name, + self.settings.namespace, + ) + else: + self.core.delete_namespaced_secret(name, self.settings.namespace) + except ApiException: + pass + + @staticmethod + def _api_error(exc: ApiException, detail: str) -> PanelError: + if exc.status == 403: + return PanelError(503, f"{detail}: Kubernetes RBAC denied the operation") + if exc.status == 409: + return PanelError(409, f"{detail}: resource conflict") + if exc.status == 404: + return PanelError(404, f"{detail}: resource not found") + return PanelError(503, detail) diff --git a/panel/backend/app/main.py b/panel/backend/app/main.py new file mode 100644 index 0000000..f077739 --- /dev/null +++ b/panel/backend/app/main.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from contextlib import asynccontextmanager +from typing import Annotated + +from fastapi import FastAPI, Query, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import FileResponse, JSONResponse, Response +from fastapi.staticfiles import StaticFiles + +from .auth_service import TelegramAuthService +from .config import settings +from .errors import PanelError +from .kubernetes_service import KubernetesService +from .models import ( + AuthResult, + CodeSubmit, + DeleteRequest, + InstanceSummary, + PasswordSubmit, + PhoneStart, + StringSessionStart, +) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + app.state.kubernetes = KubernetesService(settings) + app.state.telegram = TelegramAuthService(settings.auth_ttl_seconds) + yield + await app.state.telegram.close() + + +app = FastAPI( + title="Userbot Kubernetes Control", + version="1.0.0", + lifespan=lifespan, +) + + +@app.exception_handler(PanelError) +async def panel_error_handler(_request: Request, exc: PanelError) -> JSONResponse: + return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) + + +@app.exception_handler(RequestValidationError) +async def validation_error_handler( + _request: Request, + exc: RequestValidationError, +) -> JSONResponse: + errors = [ + { + "loc": error.get("loc", ()), + "msg": error.get("msg", "Invalid value"), + "type": error.get("type", "value_error"), + } + for error in exc.errors() + ] + return JSONResponse(status_code=422, content={"detail": errors}) + + +def kube(request: Request) -> KubernetesService: + return request.app.state.kubernetes + + +def telegram(request: Request) -> TelegramAuthService: + return request.app.state.telegram + + +@app.get("/api/health") +def health(request: Request) -> dict[str, object]: + service = kube(request) + try: + service.apps.list_namespaced_deployment( + settings.namespace, + limit=1, + ) + except Exception: + return {"ok": False, "kubernetes": False} + return {"ok": True, "kubernetes": True} + + +@app.get("/api/instances", response_model=list[InstanceSummary]) +def list_instances( + request: Request, + query: str = "", + status: str = "", +) -> list[InstanceSummary]: + return kube(request).list_instances(query=query, status=status) + + +@app.get("/api/instances/{instance_id}", response_model=InstanceSummary) +def get_instance(instance_id: str, request: Request) -> InstanceSummary: + return kube(request).get_instance(instance_id) + + +@app.get("/api/instances/{instance_id}/logs") +def get_logs( + instance_id: str, + request: Request, + tail: Annotated[int, Query(ge=1, le=1000)] = 250, +) -> dict[str, str]: + return {"logs": kube(request).logs(instance_id, tail)} + + +@app.post("/api/instances/{instance_id}/start", response_model=InstanceSummary) +def start_instance(instance_id: str, request: Request) -> InstanceSummary: + return kube(request).scale(instance_id, 1) + + +@app.post("/api/instances/{instance_id}/stop", response_model=InstanceSummary) +def stop_instance(instance_id: str, request: Request) -> InstanceSummary: + return kube(request).scale(instance_id, 0) + + +@app.post("/api/instances/{instance_id}/restart", response_model=InstanceSummary) +def restart_instance(instance_id: str, request: Request) -> InstanceSummary: + return kube(request).restart(instance_id) + + +@app.post("/api/instances/{instance_id}/delete", status_code=204) +def delete_instance( + instance_id: str, + payload: DeleteRequest, + request: Request, +) -> Response: + if payload.confirmation != instance_id: + raise PanelError(422, "Type the instance id exactly to confirm deletion") + kube(request).delete(instance_id, delete_data=payload.delete_data) + return Response(status_code=204) + + +@app.post("/api/auth/phone/start", response_model=AuthResult) +async def auth_phone_start( + payload: PhoneStart, + request: Request, +) -> AuthResult: + service = kube(request) + service.ensure_prerequisites() + service.assert_available(payload.instance_id) + flow_id = await telegram(request).start_phone(payload) + return AuthResult(status="code_required", flow_id=flow_id) + + +@app.post("/api/auth/phone/{flow_id}/code", response_model=AuthResult) +async def auth_phone_code( + flow_id: str, + payload: CodeSubmit, + request: Request, +) -> AuthResult: + authorized = await telegram(request).submit_code(flow_id, payload.code) + if authorized is None: + return AuthResult(status="password_required", flow_id=flow_id) + instance = kube(request).provision(authorized.account, authorized.session_string) + return AuthResult(status="ready", instance=instance) + + +@app.post("/api/auth/phone/{flow_id}/password", response_model=AuthResult) +async def auth_phone_password( + flow_id: str, + payload: PasswordSubmit, + request: Request, +) -> AuthResult: + authorized = await telegram(request).submit_password(flow_id, payload.password) + instance = kube(request).provision(authorized.account, authorized.session_string) + return AuthResult(status="ready", instance=instance) + + +@app.post("/api/auth/string-session", response_model=AuthResult) +async def auth_string_session( + payload: StringSessionStart, + request: Request, +) -> AuthResult: + service = kube(request) + service.ensure_prerequisites() + service.assert_available(payload.instance_id) + authorized = await telegram(request).validate_string_session(payload) + instance = service.provision(authorized.account, authorized.session_string) + return AuthResult(status="ready", instance=instance) + + +static_dir = settings.static_dir +assets_dir = static_dir / "assets" +if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + + +@app.get("/", include_in_schema=False) +def index() -> FileResponse: + return FileResponse(static_dir / "index.html") + + +@app.get("/{path:path}", include_in_schema=False) +def spa_fallback(path: str) -> FileResponse: + candidate = (static_dir / path).resolve() + if candidate.is_file() and static_dir.resolve() in candidate.parents: + return FileResponse(candidate) + return FileResponse(static_dir / "index.html") diff --git a/panel/backend/app/models.py b/panel/backend/app/models.py new file mode 100644 index 0000000..9a59d3e --- /dev/null +++ b/panel/backend/app/models.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, Field, field_validator + +INSTANCE_PATTERN = r"^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$" + + +class Resources(BaseModel): + storage: str = "1Gi" + cpu_limit: str = "300m" + memory_limit: str = "1536Mi" + + +class AccountBase(BaseModel): + instance_id: str = Field(pattern=INSTANCE_PATTERN, max_length=40) + display_name: str = Field(min_length=1, max_length=80) + api_id: int = Field(gt=0) + api_hash: str = Field(min_length=16, max_length=128) + resources: Resources = Field(default_factory=Resources) + + @field_validator("display_name", "api_hash") + @classmethod + def strip_text(cls, value: str) -> str: + return value.strip() + + +class PhoneStart(AccountBase): + phone: str = Field(min_length=7, max_length=32) + + @field_validator("phone") + @classmethod + def normalize_phone(cls, value: str) -> str: + value = value.strip() + if not value.startswith("+"): + raise ValueError("phone must use international format") + return value + + +class StringSessionStart(AccountBase): + session_string: str = Field(min_length=32) + + +class CodeSubmit(BaseModel): + code: str = Field(min_length=3, max_length=12) + + @field_validator("code") + @classmethod + def normalize_code(cls, value: str) -> str: + return "".join(value.split()) + + +class PasswordSubmit(BaseModel): + password: str = Field(min_length=1, max_length=256) + + +class DeleteRequest(BaseModel): + confirmation: str + delete_data: bool = False + + +class InstanceSummary(BaseModel): + instance_id: str + display_name: str + namespace: str + deployment: str + pod: str | None = None + status: Literal["running", "stopped", "pending", "error"] + reason: str | None = None + ready: bool = False + restarts: int = 0 + pvc: str | None = None + storage: str | None = None + image: str + cpu_limit: str | None = None + memory_limit: str | None = None + cpu_usage: str | None = None + memory_usage: str | None = None + updated_at: datetime | None = None + legacy: bool = False + deletable: bool = True + + +class AuthResult(BaseModel): + status: Literal["code_required", "password_required", "ready"] + flow_id: str | None = None + instance: InstanceSummary | None = None diff --git a/panel/backend/requirements-dev.txt b/panel/backend/requirements-dev.txt new file mode 100644 index 0000000..eea8e35 --- /dev/null +++ b/panel/backend/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +httpx==0.28.1 +pytest==8.4.1 +pytest-asyncio==1.1.0 diff --git a/panel/backend/requirements.txt b/panel/backend/requirements.txt new file mode 100644 index 0000000..c7fd098 --- /dev/null +++ b/panel/backend/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.115.12 +kubernetes==33.1.0 +pyrofork==2.3.68 +tgcrypto==1.2.5 +uvicorn[standard]==0.34.2 diff --git a/panel/backend/tests/test_auth_service.py b/panel/backend/tests/test_auth_service.py new file mode 100644 index 0000000..a17af68 --- /dev/null +++ b/panel/backend/tests/test_auth_service.py @@ -0,0 +1,84 @@ +from types import SimpleNamespace + +import pytest +from app.auth_service import TelegramAuthService +from app.models import PhoneStart, StringSessionStart + + +class FakeClient: + instances = [] + requires_password = False + + def __init__(self, *_args, **kwargs): + self.kwargs = kwargs + self.is_connected = False + self.disconnected = False + self.__class__.instances.append(self) + + async def connect(self): + self.is_connected = True + + async def disconnect(self): + self.is_connected = False + self.disconnected = True + + async def send_code(self, _phone): + return SimpleNamespace(phone_code_hash="hash") + + async def sign_in(self, *_args): + if self.requires_password: + from pyrogram.errors import SessionPasswordNeeded + + raise SessionPasswordNeeded() + + async def check_password(self, _password): + return None + + async def get_me(self): + return SimpleNamespace(id=1) + + async def export_session_string(self): + return "exported-session" + + +def phone_payload() -> PhoneStart: + return PhoneStart( + instance_id="test", + display_name="Test", + api_id=123, + api_hash="0123456789abcdef", + phone="+421900000000", + ) + + +@pytest.mark.asyncio +async def test_phone_code_success_closes_and_forgets_client(monkeypatch) -> None: + monkeypatch.setattr("app.auth_service.Client", FakeClient) + auth = TelegramAuthService() + + flow_id = await auth.start_phone(phone_payload()) + result = await auth.submit_code(flow_id, "12345") + + assert result.session_string == "exported-session" + assert flow_id not in auth.flows + assert FakeClient.instances[-1].disconnected is True + assert FakeClient.instances[-1].kwargs["in_memory"] is True + assert FakeClient.instances[-1].kwargs["no_updates"] is True + + +@pytest.mark.asyncio +async def test_string_session_is_validated_and_closed(monkeypatch) -> None: + monkeypatch.setattr("app.auth_service.Client", FakeClient) + auth = TelegramAuthService() + payload = StringSessionStart( + instance_id="test", + display_name="Test", + api_id=123, + api_hash="0123456789abcdef", + session_string="x" * 64, + ) + + result = await auth.validate_string_session(payload) + + assert result.session_string == "exported-session" + assert FakeClient.instances[-1].disconnected is True diff --git a/panel/backend/tests/test_kubernetes_service.py b/panel/backend/tests/test_kubernetes_service.py new file mode 100644 index 0000000..c782f41 --- /dev/null +++ b/panel/backend/tests/test_kubernetes_service.py @@ -0,0 +1,179 @@ +from datetime import UTC, datetime +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest +from app.config import Settings +from app.errors import PanelError +from app.kubernetes_service import KubernetesService +from app.models import AccountBase +from kubernetes.client.exceptions import ApiException + + +def service() -> KubernetesService: + core = Mock() + apps = Mock() + custom = Mock() + custom.get_namespaced_custom_object.side_effect = ApiException(status=404) + return KubernetesService(Settings(), core=core, apps=apps, custom=custom) + + +def account() -> AccountBase: + return AccountBase( + instance_id="test-account", + display_name="Test Account", + api_id=12345, + api_hash="0123456789abcdef0123456789abcdef", + ) + + +def test_renders_managed_resources_without_leaking_credentials() -> None: + kube = service() + names = kube._resource_names("test-account") + secret = kube._secret(account(), "SESSION", names) + pvc = kube._pvc(account(), names) + deployment = kube._deployment(account(), names) + + assert secret.string_data == { + "API_ID": "12345", + "API_HASH": "0123456789abcdef0123456789abcdef", + "STRINGSESSION": "SESSION", + } + assert pvc.spec.storage_class_name == "local-path-retain" + assert pvc.spec.resources.requests["storage"] == "1Gi" + assert deployment.spec.strategy.type == "Recreate" + assert deployment.spec.replicas == 1 + assert deployment.spec.template.spec.automount_service_account_token is False + assert deployment.spec.template.spec.service_account_name == "userbot-runtime" + assert deployment.spec.template.spec.volumes[1].host_path.path.endswith("/Downloads") + assert deployment.spec.template.spec.containers[0].resources.requests == { + "cpu": "80m", + "memory": "512Mi", + } + + +def test_collision_is_reported_before_auth() -> None: + kube = service() + kube.apps.read_namespaced_deployment.return_value = object() + + with pytest.raises(PanelError) as error: + kube.assert_available("test-account") + + assert error.value.status_code == 409 + assert "Deployment" in error.value.detail + + +def test_partial_provision_rolls_back_only_created_resources() -> None: + kube = service() + kube.ensure_prerequisites = Mock() + kube.assert_available = Mock() + kube.core.create_namespaced_persistent_volume_claim.side_effect = ApiException(status=500) + + with pytest.raises(PanelError): + kube.provision(account(), "SESSION") + + kube.core.delete_namespaced_secret.assert_called_once_with( + "userbot-test-account-credentials", + "userbot", + ) + kube.core.delete_namespaced_persistent_volume_claim.assert_not_called() + kube.apps.delete_namespaced_deployment.assert_not_called() + + +def test_delete_retains_pvc_unless_explicitly_requested() -> None: + kube = service() + deployment = SimpleNamespace( + metadata=SimpleNamespace( + name="userbot-test-account", + annotations={ + "userbot.forust.xyz/credentials-secret": "credentials", + "userbot.forust.xyz/pvc": "data", + }, + ) + ) + kube._find_deployment = Mock(return_value=("userbot", deployment)) + + kube.delete("test-account", delete_data=False) + + kube.apps.delete_namespaced_deployment.assert_called_once() + kube.core.delete_namespaced_secret.assert_called_once_with("credentials", "userbot") + kube.core.delete_namespaced_persistent_volume_claim.assert_not_called() + + kube.delete("test-account", delete_data=True) + kube.core.delete_namespaced_persistent_volume_claim.assert_called_once_with( + "data", + "userbot", + ) + + +def test_legacy_delete_is_blocked() -> None: + kube = service() + deployment = SimpleNamespace( + metadata=SimpleNamespace( + name="forust-userbot-deployment", + annotations={"userbot.forust.xyz/legacy": "true"}, + ) + ) + kube._find_deployment = Mock(return_value=("default", deployment)) + + with pytest.raises(PanelError) as error: + kube.delete("forust", delete_data=False) + + assert error.value.status_code == 409 + + +def pod(*, ready: bool, phase: str = "Running", reason: str | None = None): + waiting = SimpleNamespace(reason=reason) if reason else None + state = SimpleNamespace(waiting=waiting, terminated=None) + status = SimpleNamespace( + container_statuses=[ + SimpleNamespace(ready=ready, restart_count=2, state=state), + ], + phase=phase, + start_time=datetime.now(UTC), + ) + return SimpleNamespace( + metadata=SimpleNamespace(name="pod-1", creation_timestamp=datetime.now(UTC)), + status=status, + ) + + +def deployment(replicas: int = 1): + resources = SimpleNamespace(limits={"cpu": "300m", "memory": "1536Mi"}) + container = SimpleNamespace(image="userbot:latest", resources=resources) + template_spec = SimpleNamespace(containers=[container], volumes=[]) + return SimpleNamespace( + metadata=SimpleNamespace( + name="userbot-test-account", + labels={"app.kubernetes.io/instance": "test-account"}, + annotations={}, + creation_timestamp=datetime.now(UTC), + ), + spec=SimpleNamespace( + replicas=replicas, + selector=SimpleNamespace(match_labels={"app": "test"}), + template=SimpleNamespace(spec=template_spec), + ), + status=SimpleNamespace(available_replicas=1 if replicas else 0), + ) + + +@pytest.mark.parametrize( + ("replicas", "pod_value", "expected"), + [ + (0, None, "stopped"), + (1, pod(ready=True), "running"), + (1, pod(ready=False, phase="Pending"), "pending"), + (1, pod(ready=False, reason="CrashLoopBackOff"), "error"), + (1, pod(ready=False, phase="Failed"), "error"), + ], +) +def test_status_classification(replicas, pod_value, expected) -> None: + kube = service() + kube._pods_for_deployment = Mock(return_value=[pod_value] if pod_value else []) + kube._pvc_storage = Mock(return_value=None) + kube._pod_metrics = Mock(return_value=(None, None)) + + result = kube._summarize("userbot", deployment(replicas)) + + assert result.status == expected diff --git a/panel/backend/tests/test_models.py b/panel/backend/tests/test_models.py new file mode 100644 index 0000000..7c7864e --- /dev/null +++ b/panel/backend/tests/test_models.py @@ -0,0 +1,44 @@ +import pytest +from app.main import validation_error_handler +from app.models import AccountBase, DeleteRequest +from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError + + +@pytest.mark.parametrize( + "instance_id", + ["Upper", "has_space", "-leading", "trailing-", "x" * 41], +) +def test_invalid_instance_ids(instance_id: str) -> None: + with pytest.raises(ValidationError): + AccountBase( + instance_id=instance_id, + display_name="Test", + api_id=1, + api_hash="0123456789abcdef", + ) + + +def test_delete_data_defaults_to_false() -> None: + request = DeleteRequest(confirmation="test") + assert request.delete_data is False + + +@pytest.mark.asyncio +async def test_validation_response_does_not_echo_secret_input() -> None: + sensitive_value = "-".join(("very", "private", "string", "session")) + error = RequestValidationError( + [ + { + "type": "string_too_short", + "loc": ("body", "session_string"), + "msg": "String should have at least 32 characters", + "input": sensitive_value, + } + ] + ) + + response = await validation_error_handler(None, error) + + assert sensitive_value.encode() not in response.body + assert b'"input"' not in response.body diff --git a/panel/frontend/index.html b/panel/frontend/index.html new file mode 100644 index 0000000..6b909f0 --- /dev/null +++ b/panel/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Userbot · Kubernetes Control + + +
+ + + diff --git a/panel/frontend/package-lock.json b/panel/frontend/package-lock.json new file mode 100644 index 0000000..bf33df6 --- /dev/null +++ b/panel/frontend/package-lock.json @@ -0,0 +1,3970 @@ +{ + "name": "userbot-panel", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "userbot-panel", + "version": "1.0.0", + "dependencies": { + "lucide-svelte": "^0.468.0", + "svelte": "^5.16.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/svelte": "^5.2.6", + "@types/node": "^22.10.5", + "jsdom": "^25.0.1", + "svelte-check": "^4.7.3", + "typescript": "~5.7.2", + "vite": "^6.0.7", + "vitest": "^2.1.8" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "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/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "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/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.1.tgz", + "integrity": "sha512-5m3B2cbqQ4TbwW6Xkh66Ntw6dD7gNc77cCxABTTesWcq9jxIzMgTk97pZx5vEtvQx8iokgi7GIphqZe+PGwcZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": 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==", + "dev": 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/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/svelte": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.4.2.tgz", + "integrity": "sha512-4o31E4HGo5BU5KwPkulNRocEden+7Tt9JYm9uhln5ajF7DULeyFA46BBWVfKJ8Ms9B3JmOFPTIiVamH7n3KpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "9.x.x || 10.x.x", + "@testing-library/svelte-core": "1.1.3" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", + "vite": "*", + "vitest": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/svelte-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@testing-library/svelte-core/-/svelte-core-1.1.3.tgz", + "integrity": "sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "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/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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "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/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/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": 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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devalue": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", + "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-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==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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/esrap": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", + "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "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": "^5.0.0", + "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/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "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/lucide-svelte": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.468.0.tgz", + "integrity": "sha512-n0ecAFtCY5LEeL+PJ1Xj4n3c2gzj8tMpak0KMGnvoSJEjCsCnRB0mekBtJZAo7beyynW9Qj5Um1KfMBAeTNplw==", + "deprecated": "Package deprecated. Please use @lucide/svelte instead.", + "license": "ISC", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT" + }, + "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/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.56.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz", + "integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "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.7.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", + "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", + "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/node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "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/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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/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==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "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==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "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/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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "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": "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/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/panel/frontend/package.json b/panel/frontend/package.json new file mode 100644 index 0000000..9c30d10 --- /dev/null +++ b/panel/frontend/package.json @@ -0,0 +1,26 @@ +{ + "name": "userbot-panel", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite --host 0.0.0.0", + "test": "vitest run" + }, + "dependencies": { + "lucide-svelte": "^0.468.0", + "svelte": "^5.16.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/svelte": "^5.2.6", + "@types/node": "^22.10.5", + "jsdom": "^25.0.1", + "svelte-check": "^4.7.3", + "typescript": "~5.7.2", + "vite": "^6.0.7", + "vitest": "^2.1.8" + } +} diff --git a/panel/frontend/src/App.svelte b/panel/frontend/src/App.svelte new file mode 100644 index 0000000..4fd83b5 --- /dev/null +++ b/panel/frontend/src/App.svelte @@ -0,0 +1,211 @@ + + +
+ { + addOpen = true; + menuOpen = false; + }} + on:close={() => (menuOpen = false)} + /> + {#if menuOpen} + + {/if} + +
+
+ +
{running} инстанса работают
+
+ userbot + +
+
+ +
+
+
+

Инстансы

+

Telegram-аккаунты и их состояние в Kubernetes

+
+
+ +
+
Всего{instances.length}
+
Работают{running}
+
Остановлены{stopped}
+
Перезапуски{restarts}
+
+ +
+ + + +
+ + {#if error} + + {/if} + + {#if loading} +
Загружаем инстансы…
+ {:else if visibleInstances.length} + (selected = event.detail)} + on:action={(event) => runAction(event.detail.instance, event.detail.action)} + /> + {:else} +
+ +

{instances.length ? 'Ничего не найдено' : 'Инстансов пока нет'}

+

{instances.length ? 'Измените поиск или фильтр.' : 'Добавьте первый Telegram-аккаунт.'}

+ {#if !instances.length} + + {/if} +
+ {/if} +
+
+
+ + (addOpen = false)} on:created={(event) => created(event.detail)} /> + (selected = null)} + on:action={(event) => runAction(event.detail.instance, event.detail.action)} + on:delete={(event) => deleteInstance(event.detail.instance, event.detail.deleteData)} +/> diff --git a/panel/frontend/src/components/AddAccountDrawer.svelte b/panel/frontend/src/components/AddAccountDrawer.svelte new file mode 100644 index 0000000..8abf351 --- /dev/null +++ b/panel/frontend/src/components/AddAccountDrawer.svelte @@ -0,0 +1,354 @@ + + +{#if open} + + +{/if} diff --git a/panel/frontend/src/components/DetailsDrawer.svelte b/panel/frontend/src/components/DetailsDrawer.svelte new file mode 100644 index 0000000..5dd5c67 --- /dev/null +++ b/panel/frontend/src/components/DetailsDrawer.svelte @@ -0,0 +1,204 @@ + + +{#if instance} +
dispatch('close')} aria-hidden="true">
+ +{/if} diff --git a/panel/frontend/src/components/InstanceTable.svelte b/panel/frontend/src/components/InstanceTable.svelte new file mode 100644 index 0000000..5a398d6 --- /dev/null +++ b/panel/frontend/src/components/InstanceTable.svelte @@ -0,0 +1,114 @@ + + +
+ + + {#each instances as instance (instance.namespace + instance.instance_id)} +
dispatch('select', instance)} + on:keydown={(event) => { + if (event.key === 'Enter' || event.key === ' ') dispatch('select', instance); + }} + role="button" + tabindex="0" + aria-label={`Открыть ${instance.display_name}`} + > + + {instance.display_name} + {instance.instance_id} · {instance.namespace} + + + + {instance.pod || instance.reason || 'Нет pod'} + + {instance.restarts} + {instance.storage || '—'} + {relativeTime(instance.updated_at)} + + + + {#if instance.status === 'stopped'} + + {:else} + + {/if} + + +
+ {/each} +
diff --git a/panel/frontend/src/components/Sidebar.svelte b/panel/frontend/src/components/Sidebar.svelte new file mode 100644 index 0000000..0afbeb5 --- /dev/null +++ b/panel/frontend/src/components/Sidebar.svelte @@ -0,0 +1,39 @@ + + + diff --git a/panel/frontend/src/components/StatusDot.svelte b/panel/frontend/src/components/StatusDot.svelte new file mode 100644 index 0000000..56de618 --- /dev/null +++ b/panel/frontend/src/components/StatusDot.svelte @@ -0,0 +1,17 @@ + + + + + {labels[status]} + diff --git a/panel/frontend/src/lib/api.ts b/panel/frontend/src/lib/api.ts new file mode 100644 index 0000000..44d8b12 --- /dev/null +++ b/panel/frontend/src/lib/api.ts @@ -0,0 +1,76 @@ +import type { AccountDraft, AuthResult, Instance } from "./types"; + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(path, init); + if (!response.ok) { + const payload = await response.json().catch(() => ({})); + throw new Error(payload.detail || response.statusText || "Request failed"); + } + if (response.status === 204) return undefined as T; + return response.json(); +} + +const json = (body: unknown): RequestInit => ({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), +}); + +export const api = { + instances(query = "", status = "") { + const params = new URLSearchParams(); + if (query) params.set("query", query); + if (status) params.set("status", status); + const suffix = params.size ? `?${params}` : ""; + return request(`/api/instances${suffix}`); + }, + instance(id: string) { + return request(`/api/instances/${encodeURIComponent(id)}`); + }, + logs(id: string) { + return request<{ logs: string }>(`/api/instances/${encodeURIComponent(id)}/logs`); + }, + action(id: string, action: "start" | "stop" | "restart") { + return request(`/api/instances/${encodeURIComponent(id)}/${action}`, { method: "POST" }); + }, + delete(id: string, deleteData: boolean) { + return request( + `/api/instances/${encodeURIComponent(id)}/delete`, + json({ confirmation: id, delete_data: deleteData }), + ); + }, + startPhone(draft: AccountDraft) { + return request( + "/api/auth/phone/start", + json({ + ...basePayload(draft), + phone: draft.phone, + }), + ); + }, + submitCode(flowId: string, code: string) { + return request(`/api/auth/phone/${encodeURIComponent(flowId)}/code`, json({ code })); + }, + submitPassword(flowId: string, password: string) { + return request(`/api/auth/phone/${encodeURIComponent(flowId)}/password`, json({ password })); + }, + stringSession(draft: AccountDraft) { + return request( + "/api/auth/string-session", + json({ + ...basePayload(draft), + session_string: draft.session_string, + }), + ); + }, +}; + +function basePayload(draft: AccountDraft) { + return { + instance_id: draft.instance_id, + display_name: draft.display_name, + api_id: Number(draft.api_id), + api_hash: draft.api_hash, + resources: draft.resources, + }; +} diff --git a/panel/frontend/src/lib/instances.test.ts b/panel/frontend/src/lib/instances.test.ts new file mode 100644 index 0000000..7fe866c --- /dev/null +++ b/panel/frontend/src/lib/instances.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; + +import { filterInstances, relativeTime } from "./instances"; +import type { Instance } from "./types"; + +const instance = (overrides: Partial): Instance => ({ + instance_id: "forust", + display_name: "Forust", + namespace: "userbot", + deployment: "userbot-forust", + pod: "userbot-forust-abc", + status: "running", + reason: null, + ready: true, + restarts: 0, + pvc: "userbot-forust-data", + storage: "1Gi", + image: "userbot:latest", + cpu_limit: "300m", + memory_limit: "1536Mi", + cpu_usage: null, + memory_usage: null, + updated_at: null, + legacy: false, + deletable: true, + ...overrides, +}); + +describe("filterInstances", () => { + it("filters by status and searchable fields", () => { + const items = [ + instance({ instance_id: "forust" }), + instance({ instance_id: "anna", display_name: "Anna", status: "stopped" }), + ]; + expect(filterInstances(items, "ann", "stopped").map((item) => item.instance_id)).toEqual(["anna"]); + }); +}); + +describe("relativeTime", () => { + it("formats recent timestamps", () => { + expect(relativeTime("2026-01-01T00:00:00Z", Date.parse("2026-01-01T00:00:45Z"))).toBe("45 сек назад"); + }); +}); diff --git a/panel/frontend/src/lib/instances.ts b/panel/frontend/src/lib/instances.ts new file mode 100644 index 0000000..34b77ac --- /dev/null +++ b/panel/frontend/src/lib/instances.ts @@ -0,0 +1,24 @@ +import type { Instance } from "./types"; + +export function filterInstances(instances: Instance[], query: string, status: string): Instance[] { + const normalized = query.trim().toLowerCase(); + return instances.filter((instance) => { + const matchesStatus = !status || instance.status === status; + const matchesQuery = + !normalized || + instance.instance_id.toLowerCase().includes(normalized) || + instance.display_name.toLowerCase().includes(normalized) || + (instance.pod || "").toLowerCase().includes(normalized); + return matchesStatus && matchesQuery; + }); +} + +export function relativeTime(value: string | null, now = Date.now()): string { + if (!value) return "—"; + const seconds = Math.max(0, Math.round((now - new Date(value).getTime()) / 1000)); + if (seconds < 60) return `${seconds} сек назад`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes} мин назад`; + const hours = Math.floor(minutes / 60); + return `${hours} ч назад`; +} diff --git a/panel/frontend/src/lib/types.ts b/panel/frontend/src/lib/types.ts new file mode 100644 index 0000000..06d14a2 --- /dev/null +++ b/panel/frontend/src/lib/types.ts @@ -0,0 +1,45 @@ +export type InstanceStatus = "running" | "stopped" | "pending" | "error"; + +export interface Instance { + instance_id: string; + display_name: string; + namespace: string; + deployment: string; + pod: string | null; + status: InstanceStatus; + reason: string | null; + ready: boolean; + restarts: number; + pvc: string | null; + storage: string | null; + image: string; + cpu_limit: string | null; + memory_limit: string | null; + cpu_usage: string | null; + memory_usage: string | null; + updated_at: string | null; + legacy: boolean; + deletable: boolean; +} + +export interface Resources { + storage: string; + cpu_limit: string; + memory_limit: string; +} + +export interface AccountDraft { + instance_id: string; + display_name: string; + api_id: string; + api_hash: string; + phone: string; + session_string: string; + resources: Resources; +} + +export interface AuthResult { + status: "code_required" | "password_required" | "ready"; + flow_id?: string; + instance?: Instance; +} diff --git a/panel/frontend/src/main.ts b/panel/frontend/src/main.ts new file mode 100644 index 0000000..ff37c97 --- /dev/null +++ b/panel/frontend/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from "svelte"; + +import App from "./App.svelte"; +import "./styles.css"; + +mount(App, { + target: document.getElementById("app")!, +}); diff --git a/panel/frontend/src/styles.css b/panel/frontend/src/styles.css new file mode 100644 index 0000000..1f3dc8d --- /dev/null +++ b/panel/frontend/src/styles.css @@ -0,0 +1,1235 @@ +:root { + font-family: + Inter, + "SF Pro Display", + "Helvetica Neue", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + color: #f5f5f5; + background: #080808; + font-synthesis: none; + --bg: #080808; + --sidebar: #0b0b0b; + --surface: #101010; + --surface-2: #151515; + --surface-3: #1a1a1a; + --line: #2b2b2b; + --line-soft: rgba(255, 255, 255, 0.09); + --text: #f5f5f5; + --text-2: #b7b7b7; + --muted: #747474; + --accent-soft: rgba(255, 255, 255, 0.08); + --radius-sm: 6px; + --radius-md: 8px; + --sidebar-width: 204px; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + min-width: 320px; + min-height: 100%; + margin: 0; +} + +body { + min-height: 100vh; + background: var(--bg); + color: var(--text); + font-size: 13px; +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + color: inherit; +} + +button, +select { + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.42; +} + +.app-shell { + display: grid; + min-height: 100vh; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); +} + +aside[aria-label="Навигация"] { + position: fixed; + z-index: 20; + inset: 0 auto 0 0; + display: flex; + width: var(--sidebar-width); + flex-direction: column; + border-right: 1px solid var(--line); + background: var(--sidebar); + padding: 17px 10px 15px; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + min-height: 43px; + padding: 0 9px; +} + +.brand div { + display: grid; + gap: 2px; +} + +.brand strong { + font-size: 16px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.brand span, +.cluster-state span { + color: var(--muted); + font-size: 11px; +} + +.brand .mobile-close { + display: none; + margin-left: auto; +} + +nav { + display: grid; + gap: 7px; + margin-top: 29px; +} + +.nav-item { + display: flex; + height: 45px; + align-items: center; + gap: 11px; + border: 1px solid transparent; + border-radius: var(--radius-sm); + background: transparent; + color: #9b9b9b; + padding: 0 12px; + text-align: left; + font-size: 12px; + font-weight: 600; +} + +.nav-item:hover { + color: var(--text); + background: rgba(255, 255, 255, 0.035); +} + +.nav-item.active { + border-color: #3b3b3b; + color: var(--text); + background: var(--accent-soft); +} + +.cluster-state { + display: flex; + align-items: flex-start; + gap: 9px; + margin-top: auto; + border-top: 1px solid var(--line); + padding: 14px 9px 0; +} + +.cluster-state div { + display: grid; + gap: 3px; +} + +.cluster-state strong { + font-size: 11px; + font-weight: 600; +} + +.cluster-dot { + width: 9px; + height: 9px; + margin-top: 2px; + border-radius: 50%; + background: #8fce63; + box-shadow: 0 0 0 3px rgba(143, 206, 99, 0.05); +} + +.workspace { + min-width: 0; + grid-column: 2; +} + +.topbar { + position: sticky; + z-index: 10; + top: 0; + display: flex; + height: 57px; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--line); + background: rgba(8, 8, 8, 0.96); + padding: 0 22px; + backdrop-filter: blur(12px); +} + +.live-state { + display: flex; + align-items: center; + gap: 9px; + color: var(--text-2); + font-size: 11px; + font-weight: 600; +} + +.live-state span { + width: 8px; + height: 8px; + border-radius: 50%; + background: #bdbdbd; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.namespace { + display: flex; + height: 35px; + align-items: center; + gap: 7px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + color: var(--text-2); + padding: 0 12px; +} + +button.primary, +.primary { + display: inline-flex; + min-height: 35px; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px solid #e9e9e9; + border-radius: var(--radius-sm); + background: #ededed; + color: #111; + padding: 0 14px; + font-size: 12px; + font-weight: 650; +} + +button.primary:hover { + background: #fff; +} + +.content { + width: 100%; + max-width: 1280px; + margin: 0 auto; + padding: 21px 22px 48px; +} + +.page-heading h1 { + margin: 0; + font-size: 23px; + font-weight: 700; + letter-spacing: -0.025em; +} + +.page-heading p, +.drawer-header p { + margin: 5px 0 0; + color: var(--muted); + font-size: 11px; +} + +.summary-strip { + display: grid; + min-height: 62px; + grid-template-columns: repeat(4, 1fr); + margin-top: 18px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--surface); +} + +.summary-strip div { + display: flex; + align-items: center; + justify-content: space-between; + border-right: 1px solid var(--line); + padding: 0 22px; +} + +.summary-strip div:last-child { + border-right: 0; +} + +.summary-strip span { + color: var(--muted); + font-size: 11px; +} + +.summary-strip strong { + font-size: 18px; + font-weight: 650; +} + +.toolbar { + display: grid; + grid-template-columns: minmax(230px, 330px) 160px 36px; + gap: 10px; + margin-top: 17px; +} + +.search-field, +.toolbar select { + height: 36px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text-2); +} + +.search-field { + display: flex; + align-items: center; + gap: 9px; + padding: 0 12px; +} + +.search-field input { + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: var(--text); +} + +.search-field input::placeholder { + color: var(--muted); +} + +.toolbar select { + appearance: none; + background-image: + linear-gradient(45deg, transparent 50%, #777 50%), linear-gradient(135deg, #777 50%, transparent 50%); + background-position: + calc(100% - 15px) 15px, + calc(100% - 11px) 15px; + background-repeat: no-repeat; + background-size: 4px 4px; + padding: 0 30px 0 12px; +} + +.icon-button { + display: inline-flex; + width: 32px; + height: 32px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text-2); + padding: 0; +} + +.icon-button:hover:not(:disabled) { + border-color: #4a4a4a; + color: var(--text); + background: var(--surface-2); +} + +.icon-button.borderless { + border-color: transparent; + background: transparent; +} + +.refresh { + width: 36px; + height: 36px; +} + +.table-frame { + overflow: hidden; + margin-top: 14px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--surface); +} + +.table-grid { + display: grid; + grid-template-columns: + minmax(125px, 1.15fr) minmax(100px, 0.8fr) minmax(170px, 1.45fr) + 70px 85px 105px 132px; + align-items: center; + gap: 10px; +} + +.table-head { + min-height: 35px; + border-bottom: 1px solid var(--line); + color: var(--muted); + padding: 0 12px; + font-size: 9px; + font-weight: 650; +} + +.table-head .right { + text-align: right; +} + +.instance-row { + width: 100%; + min-height: 58px; + border: 0; + border-bottom: 1px solid var(--line); + background: transparent; + color: var(--text-2); + padding: 0 10px 0 12px; + text-align: left; + font-size: 11px; +} + +.instance-row:last-child { + border-bottom: 0; +} + +.instance-row:hover, +.instance-row:focus-visible { + outline: 0; + background: rgba(255, 255, 255, 0.045); +} + +.instance-row.error-row { + background: rgba(255, 255, 255, 0.025); +} + +.instance-name { + display: grid; + gap: 4px; + min-width: 0; +} + +.instance-name strong { + overflow: hidden; + color: var(--text); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.instance-name small { + overflow: hidden; + color: var(--muted); + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pod-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.row-actions { + display: flex; + justify-content: flex-end; + gap: 5px; +} + +.row-actions .icon-button { + width: 29px; + height: 29px; +} + +.status { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--text-2); + font-size: 11px; +} + +.status-dot { + width: 8px; + height: 8px; + border: 1px solid #8b8b8b; + border-radius: 50%; +} + +.status-running .status-dot { + border-color: #d0d0d0; + background: #d0d0d0; +} + +.status-pending .status-dot { + border-color: #9a9a9a; + background: linear-gradient(90deg, #9a9a9a 50%, transparent 50%); +} + +.status-error { + color: #ddd; +} + +.status-error .status-dot { + border-color: #eee; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12); +} + +.center-state, +.empty-state { + display: flex; + min-height: 300px; + flex-direction: column; + align-items: center; + justify-content: center; + color: var(--muted); + gap: 9px; + text-align: center; +} + +.empty-state h2 { + margin: 4px 0 0; + color: var(--text); + font-size: 15px; +} + +.empty-state p { + margin: 0 0 8px; +} + +.error-banner { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 13px; + border: 1px solid #414141; + border-radius: var(--radius-sm); + background: #151515; + padding: 10px 12px; + color: #d8d8d8; +} + +.error-banner button, +.text-button, +.back-link { + border: 0; + background: transparent; + color: var(--text-2); + padding: 0; + font-size: 11px; + text-decoration: underline; + text-underline-offset: 3px; +} + +.drawer-backdrop { + position: fixed; + z-index: 39; + inset: 0; + background: rgba(0, 0, 0, 0.58); +} + +.drawer { + position: fixed; + z-index: 40; + top: 0; + right: 0; + display: flex; + width: min(460px, 100vw); + height: 100dvh; + flex-direction: column; + border-left: 1px solid var(--line); + background: #0d0d0d; + box-shadow: -20px 0 60px rgba(0, 0, 0, 0.42); + animation: drawer-in 160ms ease-out; +} + +.details-drawer { + width: min(420px, 100vw); +} + +.drawer-header { + display: flex; + min-height: 76px; + align-items: flex-start; + justify-content: space-between; + border-bottom: 1px solid var(--line); + padding: 20px 22px 16px; +} + +.drawer-header h2 { + margin: 0; + font-size: 20px; + letter-spacing: -0.025em; +} + +.drawer-header .status { + margin-top: 8px; +} + +.steps { + display: grid; + grid-template-columns: repeat(3, 1fr); + margin: 0; + border-bottom: 1px solid var(--line); + padding: 0 22px; + list-style: none; +} + +.steps li { + position: relative; + display: flex; + height: 55px; + align-items: center; + gap: 7px; + border-bottom: 2px solid transparent; + color: var(--muted); + font-size: 10px; + font-weight: 600; +} + +.steps li span { + display: inline-flex; + width: 17px; + height: 17px; + align-items: center; + justify-content: center; + border: 1px solid #4a4a4a; + border-radius: 50%; + font-size: 9px; +} + +.steps li.active { + border-color: #ededed; + color: var(--text); +} + +.steps li.active span, +.steps li.complete span { + border-color: #ededed; + background: #ededed; + color: #111; +} + +.drawer-form { + overflow-y: auto; + padding: 17px 22px 20px; +} + +fieldset { + display: grid; + gap: 11px; + margin: 0; + border: 0; + border-bottom: 1px solid var(--line); + padding: 0 0 15px; +} + +fieldset + fieldset { + margin-top: 15px; +} + +legend, +.drawer-scroll h3, +.auth-step h3, +.creating-state h3 { + margin: 0 0 10px; + color: var(--text); + font-size: 12px; + font-weight: 650; +} + +label { + display: grid; + gap: 6px; + color: var(--text-2); + font-size: 10px; + font-weight: 550; +} + +label small { + color: var(--muted); + font-size: 9px; + font-weight: 400; + line-height: 1.45; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid #343434; + border-radius: var(--radius-sm); + outline: 0; + background: #121212; + color: var(--text); + padding: 0 11px; + font-size: 11px; + transition: border-color 120ms ease; +} + +input, +select { + height: 37px; +} + +textarea { + resize: vertical; + min-height: 68px; + padding-top: 10px; +} + +input:focus, +select:focus, +textarea:focus { + border-color: #777; +} + +.two-columns, +.three-columns { + display: grid; + gap: 10px; +} + +.two-columns { + grid-template-columns: 0.75fr 1.25fr; +} + +.three-columns { + grid-template-columns: repeat(3, 1fr); +} + +.input-with-action { + position: relative; + display: block; +} + +.input-with-action input { + padding-right: 38px; +} + +.input-with-action button { + position: absolute; + top: 1px; + right: 1px; + display: inline-flex; + width: 36px; + height: 35px; + align-items: center; + justify-content: center; + border: 0; + background: transparent; + color: var(--muted); +} + +.segmented { + display: grid; + grid-template-columns: 1fr 1fr; + overflow: hidden; + border: 1px solid #343434; + border-radius: var(--radius-sm); +} + +.segmented button { + height: 35px; + border: 0; + border-right: 1px solid #343434; + background: transparent; + color: var(--muted); + font-size: 10px; + font-weight: 600; +} + +.segmented button:last-child { + border-right: 0; +} + +.segmented button.active { + background: var(--accent-soft); + color: var(--text); + box-shadow: inset 0 0 0 1px #777; +} + +.form-actions { + display: grid; + grid-template-columns: 0.8fr 1.2fr; + gap: 10px; + margin-top: 16px; +} + +.form-actions > button:not(.primary), +.drawer-actions > button, +.danger-button { + display: flex; + min-height: 37px; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px solid #343434; + border-radius: var(--radius-sm); + background: #111; + color: var(--text-2); + font-size: 10px; + font-weight: 600; +} + +.form-error { + margin: 12px 0 0; + border-left: 2px solid #ddd; + color: #ddd; + padding-left: 9px; + font-size: 10px; + line-height: 1.45; +} + +.auth-step { + padding: 24px 22px; +} + +.auth-step h3 { + margin-top: 34px; + font-size: 17px; +} + +.auth-step > p { + margin: -3px 0 20px; + color: var(--muted); + font-size: 11px; +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 6px; + text-decoration: none; +} + +.auth-step form { + display: grid; + gap: 13px; +} + +.code-input { + height: 51px; + letter-spacing: 0.35em; + font-size: 20px; + text-align: center; +} + +.full { + width: 100%; +} + +.creating-state { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: center; + color: var(--muted); + text-align: center; +} + +.creating-state h3 { + margin: 16px 0 0; + font-size: 16px; +} + +.creating-state p { + max-width: 270px; + line-height: 1.55; +} + +.drawer-scroll { + overflow-y: auto; + flex: 1; + padding: 0 22px; +} + +.drawer-scroll section { + border-bottom: 1px solid var(--line); + padding: 19px 0; +} + +.resource-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.resource { + display: grid; + gap: 8px; + min-height: 69px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + padding: 11px; +} + +.resource > span, +.resource small { + color: var(--muted); + font-size: 9px; + font-weight: 500; +} + +.resource strong { + font-size: 12px; +} + +.metadata-list { + display: grid; + gap: 10px; + margin: 0; +} + +.metadata-list div { + display: grid; + gap: 5px; +} + +.metadata-list dt { + color: var(--muted); + font-size: 9px; +} + +.metadata-list dd { + display: flex; + min-width: 0; + min-height: 34px; + align-items: center; + justify-content: space-between; + gap: 8px; + margin: 0; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text-2); + padding: 0 10px; + font-size: 10px; +} + +.metadata-list dd span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.metadata-list dd.wrap { + overflow-wrap: anywhere; + padding-block: 8px; +} + +.copy-button { + display: inline-flex; + align-items: center; + border: 0; + background: transparent; + color: var(--muted); + padding: 0; +} + +.section-title { + display: flex; + align-items: center; + justify-content: space-between; +} + +.logs-section pre { + overflow: auto; + max-height: 250px; + margin: 0; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: #080808; + color: #aaa; + padding: 11px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 9px; + line-height: 1.55; + white-space: pre-wrap; +} + +.danger-zone p { + color: var(--muted); + font-size: 10px; + line-height: 1.5; +} + +.checkbox-row { + display: flex; + grid-template-columns: auto 1fr; + align-items: center; + gap: 8px; + margin-top: 10px; +} + +.checkbox-row input { + width: 14px; + height: 14px; +} + +.danger-button { + width: 100%; + margin-top: 11px; +} + +.drawer-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + border-top: 1px solid var(--line); + padding: 13px 22px 16px; +} + +.drawer-actions .primary { + min-height: 37px; +} + +.drawer-actions .delete-trigger { + grid-column: span 2; + color: #8b8b8b; +} + +.mobile-menu, +.mobile-overlay { + display: none; +} + +.spin { + animation: spin 850ms linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes drawer-in { + from { + transform: translateX(18px); + opacity: 0.6; + } +} + +@media (max-width: 980px) { + .table-grid { + grid-template-columns: minmax(120px, 1fr) 100px minmax(145px, 1.25fr) 60px 75px 116px; + } + + .table-grid > :nth-child(6) { + display: none; + } + + .summary-strip div { + padding: 0 14px; + } +} + +@media (max-width: 760px) { + .app-shell { + display: block; + } + + aside[aria-label="Навигация"] { + width: 240px; + transform: translateX(-100%); + transition: transform 160ms ease; + } + + aside[aria-label="Навигация"].open { + transform: translateX(0); + } + + .brand .mobile-close { + display: inline-flex; + border: 0; + background: transparent; + color: var(--muted); + } + + .mobile-overlay { + position: fixed; + z-index: 19; + inset: 0; + display: block; + border: 0; + background: rgba(0, 0, 0, 0.65); + } + + .workspace { + width: 100%; + } + + .mobile-menu { + display: inline-flex; + } + + .topbar { + height: 55px; + padding: 0 13px; + } + + .live-state { + display: none; + } + + .namespace { + display: none; + } + + .topbar-actions { + margin-left: auto; + } + + .content { + padding: 18px 13px 32px; + } + + .summary-strip { + grid-template-columns: 1fr 1fr; + } + + .summary-strip div { + min-height: 53px; + border-bottom: 1px solid var(--line); + } + + .summary-strip div:nth-child(2) { + border-right: 0; + } + + .summary-strip div:nth-child(3), + .summary-strip div:nth-child(4) { + border-bottom: 0; + } + + .toolbar { + grid-template-columns: 1fr 120px 36px; + } + + .table-head { + display: none; + } + + .table-frame { + border-radius: var(--radius-sm); + } + + .instance-row.table-grid { + display: grid; + min-height: 75px; + grid-template-columns: minmax(0, 1fr) auto; + gap: 7px 12px; + padding: 10px 9px 10px 12px; + } + + .instance-row > :nth-child(2) { + justify-self: end; + } + + .instance-row > :nth-child(3) { + grid-column: 1; + color: var(--muted); + font-size: 9px; + } + + .instance-row > :nth-child(4), + .instance-row > :nth-child(5), + .instance-row > :nth-child(6) { + display: none; + } + + .row-actions { + grid-column: 2; + grid-row: 2; + } + + .row-actions .icon-button:first-child, + .row-actions .icon-button:last-child { + display: none; + } + + .drawer, + .details-drawer { + width: 100vw; + } +} + +@media (max-width: 480px) { + .toolbar { + grid-template-columns: 1fr 36px; + } + + .toolbar select { + grid-column: 1; + grid-row: 2; + } + + .toolbar .refresh { + grid-column: 2; + grid-row: 1; + } + + .two-columns { + grid-template-columns: 1fr; + } + + .three-columns { + gap: 7px; + } + + .drawer-header, + .drawer-form, + .auth-step, + .drawer-actions { + padding-inline: 16px; + } + + .steps, + .drawer-scroll { + padding-inline: 16px; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/panel/frontend/src/test/setup.ts b/panel/frontend/src/test/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/panel/frontend/src/test/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/panel/frontend/svelte.config.js b/panel/frontend/svelte.config.js new file mode 100644 index 0000000..d0e6448 --- /dev/null +++ b/panel/frontend/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +export default { + preprocess: vitePreprocess(), +}; diff --git a/panel/frontend/tsconfig.json b/panel/frontend/tsconfig.json new file mode 100644 index 0000000..de8f686 --- /dev/null +++ b/panel/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "bundler", + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "module": "ESNext", + "types": ["svelte", "vite/client", "vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"] +} diff --git a/panel/frontend/vite.config.ts b/panel/frontend/vite.config.ts new file mode 100644 index 0000000..d1d3221 --- /dev/null +++ b/panel/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [svelte()], + build: { + outDir: "dist", + emptyOutDir: true, + }, + server: { + proxy: { + "/api": "http://127.0.0.1:8080", + }, + }, +}); diff --git a/panel/frontend/vitest.config.ts b/panel/frontend/vitest.config.ts new file mode 100644 index 0000000..69cf664 --- /dev/null +++ b/panel/frontend/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + setupFiles: ["./src/test/setup.ts"], + }, +});